Alex é instrutor e desenvolvedor e possui experiência em Java, Kotlin, Android. Atualmente cria conteúdo no canal https://www.youtube.com/@AlexFelipeDev.
Inscreva-se em nossa Newsletter
Fique por dentro de conteúdos, insights e oportunidades do universo tech. Receba novidades e lançamentos direto no seu e-mail.
Recentemente, em 18 de maio de 2021, foi anunciada a versão release do Hilt, uma biblioteca para injeção de dependência no Android. A partir dela, evitamos boilerplates, trechos de código que se repetem ao longo do projeto, para configurar e implementar uma solução de injeção de dependência no nosso código, o que não era uma realidade com o Dagger.
Dentro dessa proposta, o Hilt é uma biblioteca bastante atrativa recomendada pela equipe de desenvolvedores do Andoid.
Neste artigo eu vou demonstrar como é possível fazer a configuração do Hilt em um projeto Android com Navigation, ViewModel, Repository, Room e Retrofit! Quer saber como fica essa configuração? Então, bora começar!
Projeto de exemplo
Para este artigo, vou considerar o uso do Ceep, um app que simula o Google Keep para cadastrar notas. Se quiser acompanhar os exemplos do artigo, você pode acessar o código-fonte ou baixar o projeto em seu estado inicial (sem a integração com o Hilt).
Este tópico é focado em explicar mais sobre o projeto. Então, caso você não tenha interesse, vá para o próximo tópico, "Preparando o ambiente".
O app lista notas com imagem, título e descrição, sejam armazenadas internamente ou recebidas via web API.
As técnicas e tecnologias utilizadas neste projeto são:
Navigation: configuração da navegação das telas
RecyclerView: listagem das notas
ConstraintLayout: ViewGroup para implementar o layout das notas
Coil: carregar imagens via requisição HTTP
Retrofit: requisição HTTP para buscar novas notas
Room: Armazenamento interno das notas
ViewModel: modelo para manter regra de negócio das Activities/Fragments
Coroutines: processamento assíncrono
View Binding: busca de views do layout de forma segura
Observações da web API
As notas apresentadas na amostra são carregadas por meio de uma web API mockada, ou seja, para que você consiga fazer a mesma simulação, você precisa utilizar um endereço de uma API em funcionamento.
Caso você implemente a sua própria ou utilize uma mockada, é necessário configurar um end-point GET para /notes. Como corpo da resposta, você deve devolver uma lista de notas:
[{"id":1,"title":"my first title","description":"my first description"},{"id":2,"title":"my second title","description":"my second description"}]
O app também funciona sem essa integração com uma web API. O grande detalhe é que ele vai mostrar no logcat que não conseguiu fazer uma comunicação:
br.com.alura.ceep E/NoteRepository: findAll: failure tofetchnew notes from API
Para adicionar notas sem a conexão com a web API, você pode acessar a CeepApp e descomentar o código do onCreate(). Na amostra de código é apresentado um exemplo de como salvar notas:
Neste exemplo, foram salvas 2 notas, e você pode modificar a quantidade conforme a sua preferência. A única orientação é que, depois de executar o app uma vez, comente ou apague essa amostra de código para que você consiga testar as mudanças e não apareçam várias notas!
Agora que passei todas as informações do projeto, vamos começar com a configuração do Hilt!
Preparando o ambiente
Para adicionar o Hilt, adicionamos os seguintes scripts:
Feitas as modificações, sincronize o projeto com o Gradle.
Configurando a Application
Como primeiro passo, precisamos anotar a Application com a @HiltAndroidApp para que o Hilt gere o código a nível de aplicação que vai fornecer as dependências para todas as entidades, desde as classes do Android framework, como também as suas classes do projeto:
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidAppclassCeepApp: Application() {
// rest of the code
}
Apenas com essa anotação fizemos a configuração mínima para configurar o Hilt.
Configurando classes para injetar dependências
Após configurar o Hilt, podemos anotar as classes que o Hilt pode injetar dependências a partir da @AndroidEntryPoint. Como por exemplo, podemos fazer a nossa primeira configuração na MainActivity:
@AndroidEntryPointclassMainActivity : AppCompatActivity() {
// rest of the code
}
A partir de agora, a nossa Activity pode receber dependências a partir do Hilt! Para um primeiro teste, podemos criar a classe TestMyFirstDI com as seguintes informações:
package br.com.alura.ceep.ui.activity
// other importsimport dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPointclassMainActivity : AppCompatActivity() {
@Injectprivatelateinitvar instance: TestMyFirstDI
overridefunonCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.i("MainActivity", "onCreate: $instance")
// rest of the code
}
}
classTestMyFirstDI@Injectconstructor()
Note que precisamos indicar qual construtor da classe deve ser injetado pelo Hilt com a anotação @Inject e, então, criamos uma property lateinit do tipo TestMyFirstDI e anotamos com a @Inject também, pois é dessa forma indicamos para o Hilt qual construtor ele vai utilizar para criar a instância!
Inclusive, a partir dessa configuração, é indicado à esquerda da property que o mapeamento com o Hilt foi feito:
Dependências na Activity não podem ser privadas! Caso configure dessa forma, ao gerar o código no processo de build, o Hilt vai indicar que não é possível!
Então, se executamos o app para conferir o valor da instância, temos o seguinte resultado:
E é dessa forma que fazemos a configuração básica de injeção de dependência com o Hilt!
Por mais que essa demonstração seja simples, sabemos que ao utilizar ferramentas para injeção de dependência no Android, procuramos configurar classes como: ViewModel, Repositories, DAO, Services etc.
Sendo assim, vamos começar a configuração de injeção de dependência do ViewModel da tela de lista de notas!
Injetando o ViewModel
Da mesma forma que fizemos na Activity, para injetar dependências no Fragment, precisamos utilizar a anotação @AndroidEntryPoint:
@AndroidEntryPointclassNotesListFragment : Fragment() {
// rest of the code
}
Então, precisamos modificar o ViewModel que queremos injetar também, a diferença é que para o ViewModel temos uma anotação específica para que seja criado a partir de um factory, a @HiltViewModel. Como primeira amostra, vamos criar um ViewModel de teste:
A partir desta anotação, as classes anotadas com @AndroidEntryPoint, são capazes de injetar o TestViewModel.
Diferente de classes comuns, em ViewModels precisamos utilizar um ViewModelProvider ou a delegated property viewModels() para injetar dependências com o Hilt:
O grande detalhe é que essa delegated property também funciona sem o Hilt! Ou seja, não temos tanto benefício com ViewModels sem dependências! Porém, ao configurar o NoteViewModel:
Observe que, dessa vez, o construtor do NoteViewModel tem uma dependência! Ao tentar buildar o projeto, temos a seguinte mensagem de problema:
[Dagger/MissingBinding] br.com.alura.ceep.repository.NoteRepository cannot be provided without an @Injectconstructor or an @Provides-annotated method.
Ela indica que para que seja possível instanciar o NoteViewModel, é necessário indicar também como é possível instanciar o NoteRepository!
Uma das maneiras de fazer isso é anotando o construtor que desejamos instância do NoteRepository com o @Inject:
classNoteRepository@Injectconstructor(
privateval dao: NoteDao,
privateval service: NoteService
) {
// rest of the code
}
Ao fazer isso, temos um novo problema ao fazer o build:
[Dagger/MissingBinding] br.com.alura.ceep.database.dao.NoteDao cannot be provided without an @Provides-annotated method.
Precisamos também indicar ao Hilt como ele cria instâncias do NoteDao e NoteService! O grande detalhe é que são interfaces, ou seja, não temos um construtor para indicar ao Hilt como ele pode criar instâncias de interface.
Módulos do Hilt
Nesses casos, utilizamos um recurso conhecido como módulo, que é uma configuração que permite ensinar ao Hilt a criar instâncias de referências que não podem ser criadas a partir de um construtor anotado.
Para configurar um módulo, precisamos de uma classe para representar o módulo. A representação de um módulo pode ser feita a partir do que ele vai configurar, por exemplo, o NoteDao faz parte da configuração do Room que é realizado à configuração do banco de dados, portanto, podemos nomear como um módulo de banco de dados:
Para iniciar a configuração, precisamos anotar a classe com @Module para indicar ao Hilt que é um módulo e também utilizar a @InstallIn que indica onde o módulo será utilizado/instalado.
@Module@InstallInclassDatabaseModule {
{
Além de anotar, a @InstallIn exige que indiquemos, via argumento, a classe do Android que vamos instalar o módulo. Isso é necessário para determinar em qual entidade do Android a instância será criada e oferecida, bem como o seu ciclo de vida.
A princípio parece bastante abstrato, mas é mais simples do que parece, pois, basicamente, instalamos módulos em classes como: Application, Activity, Fragment, ViewModel etc.
Em outras palavras, se você instala o módulo em uma Application, a instância vai ficar disponível para toda aplicação; caso seja para uma Activity, a instância será criada pela Activity e apenas seus membros terão acesso, além disso, será sincronizada com o seu ciclo de vida, ou seja, se a Activity for destruída a instância também será!
"Então qual o escopo escolhemos?"
Em casos como o banco de dados, geralmente mantemos a disponibilidade da mesma instância para toda a aplicação. Portanto, podemos instalar o módulo em nível de aplicação. Para isso, utilizamos a referência SingletonComponent:
Com o módulo configurado, o nosso próximo passo é configurar como ele vai prover as instâncias que queremos. Por exemplo, se precisamos que ele forneça a instância do NoteDao, criamos um método que retorne esse tipo e o anotamos com @Provides:
Com o método criado, precisamos instanciar o NoteDao, então, para isso, utilizamos o método getInstance() da classe AppDatabase que cria um NoteDao. O grande detalhe é que esse método precisa de um Context para criar a instância, e não temos acesso no módulo!
É nesse momento que o Hilt brilha! Pois podemos acessar um Context da aplicação via parâmetro a partir da anotação @ApplicationContext:
Ao fazer o build do projeto, ainda temos o problema com o NoteService, mas já conseguimos ver o mapeamento da NoteDao dentro do NoteRepository:
Com essa configuração, seria o suficiente para testar o app. Porém, observe que temos também o NoteService que podemos configurar de maneira similar ao que fizemos. Então, vamos criar o Módulo para o Retrofit:
Um padrão comum ao criar provider é utilizar a nomeação provideDependencyName(), ou seja, em vez de getNoteDao(), é mais comum utilizar o provideNoteDao(). Fique à vontade para manter o que preferir, mas, a partir deste ponto, vou utilizar apenas o padrão mais comum.
O grande detalhe do NoteService é que precisamos de uma instância do Retrofit. Inicialmente, podemos fazer a seguinte implementação para prover o NoteService:
@Module@InstallIn(SingletonComponent::class)classRetrofitModule {
@ProvidesfunprovideNoteService(): NoteService {
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
val client: OkHttpClient = OkHttpClient.Builder()
.addInterceptor(logging)
.build()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create())
.client(client)
.build()
return retrofit.create(NoteService::class.java)
}
}
Com essa implementação, temos todo o mapeamento necessário e podemos testar o app que ele roda sem problemas.
Otimizando as dependências
Por mais que o app funcione, ainda existem alguns detalhes nesta implementação. Por exemplo, para atualizar automaticamente a busca de novos dados com o Room, é necessário que a instância do AppDatabase seja uma instância única em todo app, isto é, um Singleton!
Da maneira como implementamos, todas as instâncias oferecidas pelo provide são novas instâncias cada vez que uma entidade do Android recebe como injeção, portanto, por padrão, são factories, um padrão que sempre fabrica uma nova instância!
Para transformar as instâncias como Singleton, utilizamos a anotação @Singleton no provide:
O grande detalhe desta configuração é que o NoteDao está sendo o Singleton em vez do AppDatabase, ou seja, somos os responsáveis em oferecer uma instância de AppDatabase como Singleton, como é o caso do método getInstance().
Para delegar essa responsabilidade para o Hilt, podemos criar um outro provider focado apenas em criar a instância do AppDatabase:
Observe que agora ensinamos o Hilt como ele pode criar um AppDatabase e, a partir dessa configuração, podemos receber instâncias de AppDatabase para configurar dependências! Como o provider do NoteDao precisa de um AppDatabase para ser criado, logo, podemos receber, via parâmetro, a sua dependência (db: AppDatabase).
Com essa mudança, não temos mais a necessidade de manter a anotação @Singleton para no provide do NoteDao, e a implementação dele fica mais simples!
Veja também que agora o AppDatabase simplifica bem o seu código dado que não precisa manter mais o getInstance():
Com essa mudança, pontos de código que usavam a maneira antiga precisam ser ajustados, como é o caso da Application (CeepApp), que tem o código que salva notas diretamente no banco dados, que podemos ajustar para acessar via injeção de dependência:
O NoteViewModelFactory também utiliza uma instância do AppDatabase, porém, não é necessário mantê-lo, dado que o Hilt é o único responsável por criar ViewModels, então, podemos removê-lo do projeto!
Da mesma maneira que fizemos com o módulo de banco de dados, podemos também fazer com o do Retrofit: