Annotation Processor no Beagle

Neste artigo você vai ver:

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:

Fluxo de funcionamento das anotações na prática, onde vão ser encontrados arquivos com anotação não processados, logo após verificar se tem algum processador para esse arquivo. Se sim vai processar o arquivo, se não vai compilar.

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.

Mulher programando em um sofá.
Foto - Luis Eduardo
Android Developer
Sou o Luis Eduardo, conhecido como B2. Fascinado por tecnologia, gosto de livros, estudar coisas novas e compartilhar conhecimento. Sou graduando em Sistemas de Informação na Faculdade Uniessa em Uberlândia e Desenvolvedor Android na ZUP desde 2019.

Artigos relacionados

Capa do artigo sobre Docker, com um homem negro programando.
DevOps
Postado em:
Capa do artigo sobre Tech Radar, onde vemos 3 circulos, um dentro do outro, dando a ideia de aneis, no canto esquerdo. A imagem é toda na cor verde claro, mas é possível ver as sombras que formam os círculos..
Desenvolvimento
Postado em:

Este site utiliza cookies para proporcionar uma experiência de navegação melhor. Consulte nossa Política de Privacidade.