Hilt Series: Introdução ao Dagger Hilt

Ramon Ribeiro Rabello
Android Dev BR
Published in
9 min readJul 7, 2020

--

Entenda passo-a-passo como integrar a mais nova engine de injeção de dependência do Android Jetpack

Photo by Jimmy Chang on Unsplash

Este é o primeiro artigo da Hilt Series, que é composta de várias partes:

Parte 1: Introdução ao Dagger Hilt

Parte 2: Architecture Components com Dagger Hilt

Parte 3: Migrando do Dagger para o Hilt (em breve)

Parte 4: Testes com Hilt (em breve)

Recentemente, a Google anunciou o Hilt, o sucessor e versão mais amigável do Dagger que promete se tornar a engine de DI de-facto do Jetpack para o Modern Android Development, com menos boilerplate no setup, mais aderência ao Architecture Components, dentre outras features. Neste primeiro artigo da série, iremos entender o histórico de injeção de dependência no Android, o contexto do Hilt, seus principais conceitos, filosofia e como configurá-lo passo-a-passo no seu projeto.

Esse post levará em consideração que o leitor possui o conhecimento básico sobre Injeção de Dependência e os building blocks do Dagger, como components, modules, bindings e scopes.

Injeção de Dependência em Android: Um breve histórico

Roboguice, AndroidAnnotations, ButterKnife

A primeira alternativa de injeção de dependência em projetos Android: o Roboguice. Ele foi uma versão enxuta do Guice, o framework de DI criado pela Google muito utilizado nos projetos da Web, como o Google App Engine. A grande novidade trazida pelo framework, foi a possibilidade de injeção de componentes Android de forma simples e direta, apenas utilizando anotações como @InjectView, @InjectResources, além de outros, sem mais a necessidade das chamadas ao findViewById().

Além disso, com ele era possível injetar suas próprias classes por meio de providers e modules, princípios que mais à frente seriam incorporados pelo Dagger, porém de forma mais componentizada e abrangente. Porém, o Roboguice tinha a desvantagem de se utilizar reflection para realizar a injeção de dependência, o que acabava influenciando na performance do app quando houvesse muitas instâncias em memória sendo providas. Vale mencionar outros frameworks como AndroidAnnotations e Butterknife, que surgiram também para trazer mais produtividade, reduzir boilerplate e acoplamentos nos projetos Android, porém eles tinham uma abordagem mais sugar syntax do que um framework de injeção de dependência propriamente dito.

Dagger

O Dagger foi criado em 2010, pela Square, e trouxe muitas novidades, como scoped bindings, multi-bindings, components, sub-components, qualifiers, dentre outras funcionalidades. Em sua primeira versão, o framework era bem promissor e chegou a ser utilizado bastante na comunidade e em projetos de grande porte. Porém, a sua grande desvantagem era a utilização de reflection para realizar a resolução de dependências em tempo de execução (apesar de já utilizar geração de código, porém em menor quantidade), o que poderia comprometer drasticamente a performance do app.

Em 2015, a Square passou o bastão para a Google, que implementou várias novidades e melhorias na versão 2.0. A nova versão utilizava annotation processing, na qual agora as classes eram geradas em tempo de compilação, o que de certa forma melhorou a performance no app, porém trouxe uma demora maior no tempo de build, por conta da geração das classes. Conforme o projeto escalava, era muito comum um processo de build durar vàrios minutos para finalizar.

Com o passar dos anos, Dagger foi se tornando uma das mais temidas e ao mesmo tempo desafiantes bibliotecas em Android, principalmente por possuir uma curva de aprendizagem alta, conceitos complexos para serem compreendidos por desenvolvedores iniciantes (ou pelos mais experientes às vezes). Além disso, com o advento do Kotlin, eram necessários alguns workarounds para fazer com que a engine funcionasse corretamente no projeto.

Dagger-Android

O Dagger, em sua versão vanilla, foi concebido para ser executado em qualquer ambiente que execute uma JVM, como aplicações standalone, web e inclusive em Android, onde ficou mais famoso. Porém, em Android o contexto é um pouco diferente e muito mais desafiador, por vários motivos. Por exemplo, os componentes Android como Activity, Service, Fragment não são instanciados manualmente, mas sim pelo próprio framework, tornando um processo mais delicado para injeção de dependência.

Na tentativa de sanar esse vão na plataforma, a Google desenvolveu o Dagger-Android, uma extensão que tinha o propósito de tornar o framework mais compatível e aderente ao ecossistema do Android. Foram criados componentes para diminuir a quantidade de boilerplate, como era o caso da anotação @ContributesAndroidInjector, DaggerApplication, AndroidInjection, dentre outras. Porém, mesmo assim, acabou gerando uma complexidade maior pois agora o desenvolvedor necessitaria dominar tanto Dagger quanto Dagger-Android em seus projetos.

Frameworks em Kotlin

Assim que Kotlin se popularizou para desenvolvimento Android, surgiram algumas alternativas de injeção de dependência (ou service locators?), como Kodein e Koin. Eles se tornaram destaques na comunidade, principalmente pois trouxeram simplicidade, facilidade e menos boilerplate. Por conta disso, tivemos muitos desenvolvedores migrando do Dagger para esses frameworks, ou já começavam um projeto do zero utilizando eles por padrão.

Nos bastidores do Hilt

Durante a apresentação “An Opinionated Guide to Dependency Injection on Android” durante o Android Dev Summit 2019, a Google deu sua opinião a respeito de como devemos aplicar Injeção de Dependência em projetos Android, quer seja de forma manual ou utilizando alguns frameworks ou service locators; e deu sua forte recomendação para utilizar Dagger! Ao término, foram demonstradas inovações que a Google vem realizando para tornar o Dagger mais amigável em Android, com menor curva de aprendizagem e tempo para setup. Aquela foi a primeira aparição do Hilt, ainda que em seu estágio embrionário.

O que é o Hilt?

Hilt em Inglês significa “punho”, a parte de uma adaga ou espada que deve-se segurar para que não se corte ao manuseá-las. Metaforicamente contextualizando para desenvolvimento Android, é o que precisamos utilizar para não nos “ferirmos” (nos conceitos, setup, tempo de build, testes) ao utilizar o Dagger.

Construído com base no Dagger, o Hilt é a nova biblioteca que a Google lançou recentemente para injeção de dependência, o mais novo integrante do Jetpack. Ao contrário do seu ancestral, ele é simples, de fácil configuração e requer muito menos boilerplate para ser configurado dentro do projeto.

Configurando o Hilt

Configurar o Hilt, basicamente, exige apenas 4 passos:

  1. Adicionar o Hilt Android Gradle Plugin
  2. Importar as dependências do Hilt
  3. Adicionar uma Hilt Android Application
  4. Adicionar @AndroidEntryPoint

Adicionar o Hilt Android Gradle Plugin

Para configurar o Hilt no projeto, primeiramente é necessário adicionar o hilt-android-gradle-plugin no build.gradle do projeto:

Apesar do plugin não ser obrigatório para que o Hilt funcione, ele permite que as anotações sejam utilizadas sem a necessidade de estender classes Hilt_* que serão geradas via annotation processing. Sem a utilização do plugin, é necessário definir as classes da seguinte maneira:

Com o plugin, basta adicionar a anotação @HiltAndroidApp e estender Application normalmente:

O plugin realiza uma transformação no bytecode gerado e adiciona automaticamente a extensão à classe Hilt_Application para você, evitando mais boilerplate.

Depois, importe o plugin adicionando o seguinte comando no build.gradle da aplicação:

Importar as dependências do Hilt

O segundo e último passo é adicionar os comandos para importar as dependências do Hilt:

Se o seu projeto utiliza Java, troque o comando kapt por annotationProcessor:

Depois disso, basta sincronizar o seu projeto e as anotações e as classes do Hilt já estarão disponíveis para serem utilizadas.

Atenção: Na época que este artigo está sendo escrito, o Hilt ainda está na versão 2.28-alpha, o que indica que a API ainda poderá sofrer modificações e testes ainda estão sendo realizados a fim de tornar a biblioteca estável. Sendo assim, não é recomendado utilizá-lo em produção! Porém, você já pode experimentá-lo em seus projetos.

Adicionar uma Hilt Android Application

Para permitir que o Hilt gere os componentes e injete as classes, é necessário anotar a Application do seu projeto com a anotação @HiltAndroidApp:

Com Dagger puro, seria necessário adicionar uma chamada para inicializar o componente DaggerApplicationComponent e realizar a injeção manualmente:

Agora imagine que você deseja registrar logs para fins de auditoria. Para injetarmos dentro da Application, basta adicionar o @Inject no construtor da classe:

E injetá-la também usando a anotação @Inject:

Aqui vale mencionar algo importante: como não temos uma inicialização explícita do componente usando Hilt, a partir de qual ponto que é possível utilizar o objeto e garantir que terá uma instância provida para ele? A resposta é: depois da chamada do método onCreate() da classe mãe, dessa maneira:

Porém, esse trecho iria gerar uma exceção ao tentar acessar a propriedade logger:

Entry Points

O Hilt trouxe um conceito novo de Entry Points, que pode ser entendido como a fronteira entre códigos que poderão ser gerenciado pelo Dagger, isto é, o local onde o as classes poderão ser injetadas. O Hilt já define a anotação @AndroidEntryPoint por padrão. Ela pode ser utilizada em componentes Android, como Activity, Service, Fragment, etc. Suponha que precise chamar o mesmo logger do exemplo anterior na MainActivity. O código ficaria da seguinte maneira:

Hilt Modules

Agora suponha que você precise adicionar uma nova dependência via módulo do Dagger que precise ser utilizada na Activity do projeto, porém com provisão via módulo Dagger:

A anotação @InstallIn é utilizada para indicar ao Hilt que este módulo será adicionado no componente padrão ActivityComponent. Essa anotação é obrigatória para que um módulo seja interpretado corretamente pelo Hilt.

Hilt Components

No trecho de código acima, como que o Hilt soube injetar a classe AnotherLogger? Qual a mágica por trás de tudo isso? O grande segredo do truque é que o Hilt possui vários componentes e escopos já definidos por padrão para serem utilizados pelos componentes Android como Activity, Fragment, Service, ViewModels, Views, sem a necessidade de criação de vários components e sub-components exigido pelo Dagger. Na imagem a seguir, é possível visualizar a hierarquia de componente do Hilt.

Hierarquia de Componentes do Hilt

Na imagem acima, cada component possui um escopo associado. Isso significa que, quando uma instância está sendo provida dentro de um determinado component, ele poderá ser injetado se ele tiver um escopo compatível com aquele componente.

No exemplo, a classe AnotherLogger está sendo fornecida dentro do módulo AuditModule que possui o escopo @ActivityScoped. Além disso, um component em um nível mais baixo da árvore herda o escopo de seu ancestral, indicado pela seta na imagem. Contextualizando, a classe AnotherLogger pode ser utilizada tanto no escopo @Singleton, da ApplicationComponent, quanto do escopo @ActivityScoped, da ActivityComponent. Ou um ViewComponent, pode ser utilizado tanto no escopo @ViewScoped quanto @ActivityScoped.

Filosofia de Design do Hilt

Toda essa estrutura é possível graças à filosofia do sistema de componentes do Hilt, que é monolítica, na qual existe apenas um único component que é responsável por gerenciar os módulos que os referenciam. Ao contrário do Dagger, que possui uma filosofia de componentes polilíticos, necessitando a definição de vários components e subcomponents. Apesar de permitir uma maior modularidade e mais separação de preocupações, essa abordagem pode deixar o código muito mais complexo e exigir mais escrita de código à medida que o projeto escala. Foi uma decisão que a Google tomou para tentar minimizar a complexidade em torno do Dagger. Neste link, é possível ler mais a respeito das decisões, prós e contras que a Google levou em consideração ao construir o Hilt.

Conclusão

Apesar de ainda estar na versão alpha, já deu para perceber o grande potencial e como o Hilt é promissor, corrigindo vários pontos negativos e dores que os desenvolvedores Android enfrentavam com Dagger, principalmente no que diz respeito ao custo da configuração inicial, quantidade de boilerplate necessária para estruturar seu projeto, integração com Kotlin e aderência ao contexto Android.

Ainda está muito cedo para afirmar que o Hilt veio para ficar, ainda existem algumas limitações, como suporte a FragmentScenario, apesar da comunidade já estar testando essa integração; mas arrisco dizer que ele pode se tornar a engine de Injeção de Dependência das próximas gerações no desenvolvimento Android. Neste link, você encontra o repositório com o código utilizado neste artigo. No próximo artigo desta série, será demonstrado como integrar o Hilt com Architecture Components.

Links

--

--