O Beagle, ferramenta open source desenvolvida pela Zup, é muito versátil. Por isso, no artigo de hoje vamos ver como usar o Annotation Processor no Beagle.
Tá, mas o que é Annotation Processor?
Annotation Processor é uma biblioteca open source em que o principal objetivo é na geração de códigos.
Para quem não conhece o Annotation Processor ele é um código que começa por @ e na frente uma palavra reservada a qual é criada em uma classe de anotação. Possivelmente, você já viu muitos casos assim, por exemplo, Biblioteca Retrofit, Api do Android, entre outras.
Assim é possível criar várias anotações, usá-las em vários trechos de código e automaticamente gerar novos arquivos.
No exemplo abaixo vamos ver como o Framework Beagle utiliza o Annotation Processor. Mas primeiro vamos ver como o Annotation Processor funciona.
Como o Annotation Processor funciona
O Annotation Processor é uma ferramenta incorporada ao Javac (Compilador primário da linguagem Java) para digitalizar e processar anotações em tempo de compilação. Ele pode criar novos arquivos de origem, mas não é possível modificar os existentes. Abaixo uma fluxo de como funciona na prática as anotações:
Problema no Beagle
O Beagle precisa de algumas configurações antes do uso como, por exemplo, configuração de camada de rede, configuração de cache entre outros. Assim temos uma classe chamada BeagleSetup que é responsável por pegar essas configurações feitas pelo usuário e fazer com que o Beagle as use.
public final class BeagleSetup : BeagleSdk {
public override val httpClient: HttpClient = br.com.zup.beagle.sample.config.HttpClientDefault()
...
}
Como podemos ver no código acima, temos uma variável chamada “httpClient” que veio sobrescrita por meio da interface BeagleSdk. Atribuímos a ela uma implementação desenvolvida pelo usuário, fazendo que o Beagle use essa configuração para realizar as chamadas de serviços.
Só que temos que fazer esse processo para todas as configurações feitas no Beagle. Por isso, utilizamos o Annotation Processor para auxiliar, usando uma anotação nas classes de configuração que o usuário cria.
Assim o Annotation Processor irá gerar este arquivo BeagleSetup e já colocar as classes de configuração que usuário criou para o Beagle.
Abaixo vamos ver como o Beagle criou essa solução com Annotation Processor.
Como Beagle utiliza o Annotation Processor
No módulo android-annotation, o beagle cria a anotação @BeagleComponent. É necessário a utilização dessa anotação em todas as classes de configuração do Beagle (HttpClient, Logger, Cache, BeagleConfig etc).
Abaixo um exemplo da classe que gera a anotação @BeagleComponent:
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class BeagleComponent
A primeira classe de anotação comum é @Target. Ela descreve em qual contexto a anotação vai ser aplicada. Em outras palavras, ela informa em quais elementos de código você pode colocar essa anotação. Por exemplo, se você utilizará apenas em classes, o seguinte valor deve ser informado:
@Target(AnnotationTarget.CLASS)
A segunda classe de anotação comum é @Retention. Ela diz ao compilador quanto tempo a anotação deve “viver”. Um exemplo de retenção seria a anotação operar apenas em tempo de execução. Para isso, basta utilizar o valor AnnotationRetention.RUNTIME como parâmetro da @Retention. A @Retention deve ser utilizada abaixo da @Target:
@Retention(AnnotationRetention.RUNTIME
Agora iremos criar a classe de anotação. Para isso, basta criar uma classe e antes da palavra reservada class adicione annotation. Após esse processo é possível usar essa anotação nas classes.
annotation class BeagleComponent
Agora temos que configurar uma nova classe onde ela será responsável em reconhecer as classes de configuração do Beagle com anotação e gerar esse novo arquivo de configuração chamado BeagleSetup.
A configuração da anotação é feita no módulo processor nas classes BeagleSetupPropertyGenerator, BeagleSetupProcessor e BeagleAnnotationProcessor. Aqui fica toda a configuração, quando encontrada a anotação ele irá gerar um novo arquivo automaticamente.
BeagleSetupPropertyGenerator
Essa classe é responsável por fazer as configurações e gerar os atributos da classe BeagleSetup. Isso, pois a classe herda da interface BeagleSdk que sobrescreve alguns atributos responsável por pegar a configuração criada pelo usuário e passar para o Beagle usar.
Essa classe foi criada com alguns métodos que serão usados na classe BeagleSetupProcessor para fazer a regra da classe gerada BeagleSetup.
internal class BeagleSetupPropertyGenerator(
private val processingEnv: ProcessingEnvironment)
{
...
}
BeagleSetupProcessor
Nesta classe estão as configurações da classe que será gerada. Então temos configuração de imports, nome da classe e configuração dos gerador de atributo da classe BeagleSetupPropertyGenerator.
Essa classe foi criada com alguns métodos que serão usados na classe BeagleAnnotationProcessor para fazer a regra da classe gerada BeagleSetup.
internal data class BeagleSetupProcessor(
private val processingEnv: ProcessingEnvironment,
private val beagleSetupPropertyGenerator: BeagleSetupPropertyGenerator =
BeagleSetupPropertyGenerator(processingEnv),
private val registerAnnotationProcessor: RegisterControllerProcessor =
RegisterControllerProcessor(processingEnv),
)
{
...
}
BeagleAnnotationProcessor
A classe Beagle Annotation Processor é onde vamos fazer a configuração para Annotation Processor gerar a nossa classe BeagleSetup.
Sobre a classe vemos uma anotação @AutoService(Processor.class). Essa anotação permite que você use os processadores para serem executados em tempo de compilação.
@SupportedSourceVersion(SourceVersion.RELEASE_8) indica a versão de origem mais recente que um processador de anotações suporta.
@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.ISOLATING). Essa anotação descreve o tipo de processamento de anotação incremental que o processador anotado é capaz de fazer.
Após colocar as anotações, herdamos da classe AbstractProcessor. Assim ela irá disponibilizar alguns métodos para definir o comportamento que nosso processador precisa.
override fun init(processingEnvironment: ProcessingEnvironment) {
super.init(processingEnvironment)
beagleSetupProcessor = BeagleSetupProcessor(processingEnvironment)
}
No método init temos no parâmetro o atributo processingEnvironment. Esse atributo permite você a ter acesso a Filter, Elements e Types.
override fun process(
annotations: Set<TypeElement>,
roundEnvironment: RoundEnvironment
): Boolean {
if (annotations.isEmpty() || roundEnvironment.errorRaised()) return false
val beagleConfigElements = roundEnvironment.getElementsAnnotatedWith(
BeagleComponent::class.java
).filter { element ->
val typeElement = element as TypeElement
typeElement.implements(BEAGLE_CONFIG, processingEnv)
}
when {
beagleConfigElements.size > 1 -> {
processingEnv.messager.error("BeagleConfig already defined, " +
"remove one implementation from the application.")
}
beagleConfigElements.isEmpty() -> {
processingEnv.messager.error("Did you miss to annotate your " +
"BeagleConfig class with @BeagleComponent?")
}
else -> {
val fullClassName = beagleConfigElements[0].asType().toString()
val beagleConfigClassName = fullClassName.substring(
fullClassName.lastIndexOf(".") + 1
)
val basePackageName = fullClassName.replace(".$beagleConfigClassName", "")
beagleSetupProcessor.process(basePackageName, beagleConfigClassName, roundEnvironment)
}
}
return true
}
O método processor será executado e aqui poderão ser criados os códigos compilados. Ele contém o parâmetro roundEnvironment com ele. Assim, você poderá descobrir todas as classes anotadas que serão executadas por esse processador. O parâmetro annotations representa uma classe ou elemento de uma interface que fornece acesso a informações sobre o tipo e seus membros.
override fun getSupportedAnnotationTypes(): Set<String> {
return TreeSet(listOf(
RegisterWidget::class.java.canonicalName,
BeagleComponent::class.java.canonicalName,
RegisterValidator::class.java.canonicalName,
RegisterAction::class.java.canonicalName,
RegisterController::class.java.canonicalName
))
}
Neste método deve ser retornado as anotações que você criou. Assim o processador irá reconhecer e processar essa classe anotada.
@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.ISOLATING)
class BeagleAnnotationProcessor : AbstractProcessor() {
private lateinit var beagleSetupProcessor: BeagleSetupProcessor
override fun getSupportedAnnotationTypes(): Set<String> {
return TreeSet(listOf(
RegisterWidget::class.java.canonicalName,
BeagleComponent::class.java.canonicalName,
RegisterValidator::class.java.canonicalName,
RegisterAction::class.java.canonicalName,
RegisterController::class.java.canonicalName
))
}
override fun init(processingEnvironment: ProcessingEnvironment) {
super.init(processingEnvironment)
beagleSetupProcessor = BeagleSetupProcessor(processingEnvironment)
}
override fun process(
annotations: Set<TypeElement>,
roundEnvironment: RoundEnvironment
): Boolean {
if (annotations.isEmpty() || roundEnvironment.errorRaised()) return false
val beagleConfigElements = roundEnvironment.getElementsAnnotatedWith(
BeagleComponent::class.java
).filter { element ->
val typeElement = element as TypeElement
typeElement.implements(BEAGLE_CONFIG, processingEnv)
}
when {
beagleConfigElements.size > 1 -> {
processingEnv.messager.error("BeagleConfig already defined, " +
"remove one implementation from the application.")
}
beagleConfigElements.isEmpty() -> {
processingEnv.messager.error("Did you miss to annotate your " +
"BeagleConfig class with @BeagleComponent?")
}
else -> {
val fullClassName = beagleConfigElements[0].asType().toString()
val beagleConfigClassName = fullClassName.substring(
fullClassName.lastIndexOf(".") + 1
)
val basePackageName = fullClassName.replace(".$beagleConfigClassName", "")
beagleSetupProcessor.process(basePackageName, beagleConfigClassName, roundEnvironment)
}
}
return true
}
}
Após essa configuração, quando criar uma classe de configuração do Beagle e usar a anotação @BeagleComponent sobre o nome da classe o Annotation Processor irá gerar a classe BeagleSetup passando a sua classe de configuração para o Beagle.
Abaixo um exemplo da classe BeagleSetup gerada pelo Annotation Processor.
public final class BeagleSetup : BeagleSdk {
public override val formLocalActionHandler: FormLocalActionHandler =
br.com.zup.beagle.sample.AppFormLocalActionHandler()
public override val deepLinkHandler: DeepLinkHandler =
br.com.zup.beagle.sample.AppDeepLinkHandler()
public override val httpClient: HttpClient = br.com.zup.beagle.sample.config.HttpClientDefault()
public override val designSystem: DesignSystem = br.com.zup.beagle.sample.AppDesignSystem()
public override val storeHandler: StoreHandler =
br.com.zup.beagle.sample.config.StoreHandlerDefault()
public override val urlBuilder: UrlBuilder? = null
public override val analytics: Analytics = br.com.zup.beagle.sample.AppAnalytics()
public override val analyticsProvider: AnalyticsProvider? = null
public override val logger: BeagleLogger = br.com.zup.beagle.sample.config.BeagleLoggerDefault()
public override val controllerReference: BeagleControllerReference =
ControllerReferenceGenerated()
public override val imageDownloader: BeagleImageDownloader? = null
public override val serverDrivenActivity: Class<BeagleActivity> = br.com.zup.beagle.android.view.ServerDrivenActivity::class.java as Class<BeagleActivity>
public override val config: BeagleConfig = AppBeagleConfig()
public override val typeAdapterResolver: TypeAdapterResolver = RegisteredCustomTypeAdapter
public override val validatorHandler: ValidatorHandler = RegisteredCustomValidator
public override fun registeredWidgets(): List<Class<WidgetView>> =
RegisteredWidgets.registeredWidgets()
public override fun registeredOperations(): Map<String, Operation> =
RegisteredOperations.registeredOperations()
public override fun registeredActions(): List<Class<Action>> =
RegisteredActions.registeredActions()
}
Vantagens e Desvantagens
As anotações são muito poderosas, mas temos vantagens e desvantagens.
Uma das vantagens é evitar boilerPlates e a não a necessidade de usar reflection. Já como desvantagem, temos o aumento do build time do projeto.
Mas cabe a cada um em seu próprio contexto decidir se seus prós superam seus contras.
Conclusão
As anotações se tornaram uma parte essencial no desenvolvimento Android hoje em dia. Neste artigo, utilizamos o projeto do Beagle de exemplo para ver a implementação de como foram criadas as anotações e como ele foi configurado. Espero que esse artigo esclareça tudo sobre o que é anotação e como ela pode ser usada.
Segue o link do repositório do Beagle para acessar o exemplo usado no artigo.
Referências:
com autor: Elve. Annotation Tutorial for Dummies in Kotlin. Mobile App Development, 2018. Disponível em: https://medium.com/mobile-app-development-publication/annotation-tutorial-for-dummies-in-kotlin-1da864acc442. Acesso em: 08, janeiro e 2021.
com autor: Glavas, Gordan. Annotation Processing: Supercharge Your Development. Raywenderlich, 2020 Disponível em: https://www.raywenderlich.com/8574679-annotation-processing-supercharge-your-development . Acesso em: 08, janeiro e 2021.
com autor: Theodoro, Felipe. Annotation Processing no Android. Felipe Theodoro, 2016. Disponível em: https://medium.com/android-dev-br/annotation-processing-no-android-d28b734b8043. Acesso em: 08, janeiro e 2021.