Neste codelab, você aprenderá a importância da injeção de dependência (DI, na sigla em inglês) para criar um aplicativo sólido e extensível que se ajusta a grandes projetos. Usaremos o Hilt como a ferramenta de DI para gerenciar dependências.
A injeção de dependência é uma técnica amplamente usada na programação e adequada para o desenvolvimento em Android. Ao seguir os princípios da DI, você cria a base para uma boa arquitetura do app.
A implementação da injeção de dependência oferece as seguintes vantagens:
- Reutilização do código
- Facilidade de refatoração
- Facilidade de teste
O Hilt é uma biblioteca de injeção de dependência para Android que reduz o código boilerplate criado quando a DI é implementada manualmente no projeto. A injeção manual de dependência requer a construção de cada classe e das respectivas dependências, além do uso de contêineres para reutilizar e gerenciar as dependências.
O Hilt oferece uma maneira padrão de fazer a DI no aplicativo, fornecendo contêineres para cada componente do Android no projeto e gerenciando o ciclo de vida do contêiner automaticamente. Para fazer isso, usamos a Dagger, uma biblioteca de DI bastante conhecida.
Se você encontrar algum problema (bugs no código, erros gramaticais, instruções pouco claras, etc.) neste codelab, informe o problema no link Informar um erro no canto inferior esquerdo do codelab.
Pré-requisitos
- Experiência com a sintaxe do Kotlin
- Saber por que a injeção de dependência é importante no aplicativo
O que você aprenderá
- Como usar o Hilt no seu app Android.
- Conceitos relevantes do Hilt para criar um app sustentável.
- Como adicionar várias vinculações a um mesmo tipo com qualificadores.
- Como usar
@EntryPoint
para acessar contêineres de classes não compatíveis com o Hilt. - Como usar testes de unidade e de instrumentação em um aplicativo que usa o Hilt.
Pré-requisitos
- Android Studio 4.0 ou versões mais recentes.
Buscar o código
Consiga o código do codelab no GitHub:
$ git clone https://github.com/googlecodelabs/android-hilt
Se preferir, faça o download do repositório como um arquivo ZIP:
Abrir o Android Studio
Esse codelab exige o Android Studio 4.0 ou versões mais recentes. Se precisar fazer o download do Android Studio, clique aqui.
Como executar o app de exemplo
Neste codelab, você adicionará o Hilt a um aplicativo que registra interações de usuários e usa a Room para armazenar dados em um banco de dados local.
Siga estas instruções para abrir o app de exemplo no Android Studio:
- Se você fez o download do arquivo ZIP, descompacte o arquivo localmente.
- Abra o projeto no Android Studio.
- Clique no botão Run
e escolha um emulador ou conecte seu dispositivo Android.
Como podemos ver, um registro é criado e armazenado toda vez que você interage com um dos botões numerados. Na tela See all logs, será exibida uma lista de todas as interações anteriores. Para remover os registros, toque no botão Delete logs.
Configuração do projeto
O projeto está disponível em várias ramificações do GitHub:
master
é a ramificação que você acessou ou transferiu por download. Este é o ponto de partida do codelab.solution
contém a solução deste codelab.
Recomendamos que você comece com o código na ramificação master
e siga o codelab passo a passo no seu ritmo.
Durante o codelab, você verá snippets de código que precisam ser adicionados ao projeto. Em alguns locais, também será necessário remover o código que é explicitamente mencionado nos comentários dos snippets de código.
Para acessar a ramificação solution
usando o git, use o seguinte comando:
$ git clone -b solution https://github.com/googlecodelabs/android-hilt
Ou faça o download do código da solução aqui:
Perguntas frequentes
Por que o Hilt?
Ao observar o código inicial, você verá uma instância da classe ServiceLocator
armazenada na classe LogApplication
. O ServiceLocator
cria e armazena dependências recebidas sob demanda pelas classes que precisam delas. É possível pensar nesse processo como um contêiner de dependências que é anexado ao ciclo de vida do app, já que será destruído com ele.
Como explicado no guia para DI do Android, os localizadores de serviços começam com relativamente pouco código boilerplate, mas não apresentam um bom escalonamento. Para desenvolver um app Android em grande escala, use o Hilt.
O Hilt remove o código boilerplate que você precisa para usar um padrão de DI manual ou do localizador de serviços em um app Android, gerando o código que você teria criado manualmente (por exemplo, o código na classe ServiceLocator
).
Nas próximas etapas, você usará o Hilt para substituir a classe ServiceLocator
. Depois disso, adicionaremos novos recursos ao projeto para explorar mais as funcionalidades do Hilt.
Hilt no projeto
O Hilt já está configurado na ramificação master
(o código que você transferiu por download). Não é necessário incluir o código a seguir no projeto. Isso já foi feito para você. No entanto, vamos ver o que é necessário para usar o Hilt em um app Android.
Além das dependências da biblioteca, o Hilt usa um plug-in do Gradle configurado no projeto. Abra o arquivo build.gradle
raiz e veja a seguinte dependência do Hilt no caminho de classe:
buildscript {
...
ext.hilt_version = '2.28-alpha'
dependencies {
...
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
Em seguida, para usar o plug-in do Gradle no módulo app
, especifique-o no arquivo app/build.gradle
adicionando o plug-in à parte superior do arquivo, abaixo do plug-in kotlin-kapt
:
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
}
Por fim, as dependências do Hilt são incluídas no nosso projeto no mesmo arquivo app/build.gradle
:
...
dependencies {
...
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}
Todas as bibliotecas, incluindo o Hilt, são transferidas por download ao criar e sincronizar o projeto. Vamos começar a usar o Hilt!
Da mesma forma que a instância de ServiceLocator
na classe LogApplication
é usada e inicializada, para adicionar um contêiner anexado ao ciclo de vida do app, precisamos anotar a classe Application
com @HiltAndroidApp
. Abra LogApplication.kt
e adicione a anotação à classe:
@HiltAndroidApp
class LogApplication : Application() {
...
}
A anotação @HiltAndroidApp
aciona a geração de código do Hilt, incluindo uma classe base para o aplicativo que pode usar injeção de dependência. O contêiner do aplicativo é o contêiner pai do app, o que significa que outros contêineres podem acessar as dependências fornecidas por ele.
Agora, nosso app está pronto para usar o Hilt.
Em vez de capturar dependências de que você precisa da instância ServiceLocator
em nossas classes, usaremos o Hilt para fornecer essas dependências para nós. Vamos começar a substituir as chamadas do ServiceLocator
nas nossas classes.
Abra o arquivo ui/LogsFragment.kt
. LogsFragment
preenche os campos dele em onAttach
. Em vez de preencher instâncias de LoggerLocalDataSource
e DateFormatter
manualmente usando o ServiceLocator
, podemos usar o Hilt para criar e gerenciar instâncias desses tipos.
Para fazer com que o LogsFragment
use o Hilt, temos que adicionar a anotação @AndroidEntryPoint
a ele:
@AndroidEntryPoint
class LogsFragment : Fragment() {
...
}
Inserir anotações de classes do Android com @AndroidEntryPoint
cria um contêiner de dependências que segue o ciclo de vida de classes do Android.
Com o @AndroidEntryPoint
, o Hilt criará um contêiner de dependências anexado ao ciclo de vida do LogsFragment
e poderá injetar instâncias no LogsFragment
. Como podemos fazer injeções nos campos com o Hilt?
Podemos fazer o Hilt injetar instâncias de diferentes tipos com a anotação @Inject
nos campos que queremos injetar (ou seja, logger
e dateFormatter
):
@AndroidEntryPoint
class LogsFragment : Fragment() {
@Inject lateinit var logger: LoggerLocalDataSource
@Inject lateinit var dateFormatter: DateFormatter
...
}
Isso é chamado de injeção de campo.
Como o Hilt é responsável por preencher esses campos, não precisaremos mais do método populateFields
. Vamos remover o método da classe:
@AndroidEntryPoint
class LogsFragment : Fragment() {
// Remove following code from LogsFragment
override fun onAttach(context: Context) {
super.onAttach(context)
populateFields(context)
}
private fun populateFields(context: Context) {
logger = (context.applicationContext as LogApplication).serviceLocator.loggerLocalDataSource
dateFormatter =
(context.applicationContext as LogApplication).serviceLocator.provideDateFormatter()
}
...
}
Internamente, o Hilt preencherá esses campos no método onAttach()
do ciclo de vida com instâncias criadas no contêiner de dependências do LogsFragment
gerado automaticamente.
Para realizar a injeção de campo, o Hilt precisa saber como fornecer instâncias dessas dependências. Nesse caso, o Hilt precisa saber como fornecer instâncias de LoggerLocalDataSource
e DateFormatter
. No entanto, o Hilt ainda não sabe como fornecer essas instâncias.
Informar ao Hilt como fornecer dependências com @Inject
Abra o arquivo ServiceLocator.kt
para ver como o ServiceLocator
é implementado. Veja que chamar o provideDateFormatter()
sempre retorna uma instância diferente de um DateFormatter
.
Esse é exatamente o comportamento que queremos que o Hilt tenha. Felizmente, o DateFormatter
não depende de outras classes, então não precisamos nos preocupar com dependências transitivas por enquanto.
Para instruir o Hilt sobre como fornecer instâncias de um tipo, adicione a anotação @Inject ao construtor da classe que você quer injetar.
Abra o arquivo util/DateFormatter.kt
e anote o construtor do DateFormatter
com @Inject
. Para anotar um construtor em Kotlin, você também precisa usar a palavra-chave constructor
:
class DateFormatter @Inject constructor() { ... }
Com isso, o Hilt saberá como fornecer instâncias do DateFormatter
. O mesmo precisa ser feito para LoggerLocalDataSource
. Abra o arquivo data/LoggerLocalDataSource.kt
e anote o construtor com @Inject
:
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
...
}
Se abrirmos a classe ServiceLocator
novamente, veremos que temos um campo LoggerLocalDataSource
público. Isso significa que o ServiceLocator
retornará à mesma instância de LoggerLocalDataSource
sempre que for chamado. Isso é o que chamamos de "definir o escopo de uma instância para um contêiner". Como podemos fazer isso no Hilt?
Podemos usar anotações para definir o escopo das instâncias nos contêineres. Como o Hilt pode produzir diferentes contêineres com ciclos de vida distintos, há anotações diferentes com escopo para esses contêineres.
A anotação que define o escopo de uma instância para o contêiner do aplicativo é @Singleton
. Essa anotação fará com que o contêiner do aplicativo sempre forneça a mesma instância, mesmo que o tipo seja usado como uma dependência de outro tipo ou caso precise ser injetado por um campo.
A mesma lógica pode ser aplicada a todos os contêineres anexados a classes do Android. Veja a lista de todas as anotações de escopo na documentação. Por exemplo, se quiser que um contêiner de atividade sempre forneça a mesma instância de um tipo, você pode anotar esse tipo com @ActivityScoped
.
Como mencionado acima, queremos que o contêiner do aplicativo sempre forneça a mesma instância de LoggerLocalDataSource
, então anotamos a classe com @Singleton
:
@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
...
}
Agora, o Hilt sabe como fornecer instâncias de LoggerLocalDataSource
e o tipo tem dependências transitivas. Para fornecer uma instância de LoggerLocalDataSource
, o Hilt também precisa saber como fornecer uma instância de LogDao
.
Já que LogDao
é uma interface, não podemos anotar o construtor com @Inject
porque as interfaces não têm um. Então, como informaremos o Hilt para fornecer instâncias desse tipo?
Os módulos são usados para adicionar vinculações ao Hilt ou, em outras palavras, informar ao Hilt como fornecer instâncias de tipos diferentes. Nos módulos do Hilt, inclua vinculações para tipos que não podem ser injetados por construtores, como interfaces ou classes que não estão contidas no projeto. Um exemplo disso é o OkHttpClient
: você precisa usar o builder dele para criar uma instância.
Um módulo Hilt é uma classe anotada com @Module
e @InstallIn
. @Module
informa ao Hilt que é um módulo e @InstallIn
informa ao Hilt em quais contêineres as vinculações estão disponíveis, especificando um componente do Hilt. Você pode imaginar um componente do Hilt como um contêiner, e a lista completa de componentes pode ser encontrada neste link.
Há um componente associado do Hilt para cada classe Android que pode ser injetada. Por exemplo, o contêiner Application
está associado a ApplicationComponent
, e o contêiner Fragment
está associado a FragmentComponent
.
Como criar um módulo
Vamos criar um módulo Hilt em que podemos adicionar vinculações. Crie um novo pacote com o nome di
no pacote hilt
e crie um novo arquivo com o nome DatabaseModule.kt
dentro do pacote.
Como LoggerLocalDataSource
tem o escopo definido no contêiner do aplicativo, a vinculação LogDao
precisa estar disponível no contêiner do aplicativo. Especificamos esse requisito usando a anotação @InstallIn
transmitindo a classe do componente Hilt associado a ele (ou seja, ApplicationComponent:class
):
package com.example.android.hilt.di
@InstallIn(ApplicationComponent::class)
@Module
object DatabaseModule {
}
Na implementação da classe ServiceLocator
, a instância de LogDao
é recebida ao chamar logsDatabase.logDao()
. Portanto, para fornecer uma instância de LogDao, precisamos de uma dependência transitiva na classe AppDatabase
.
Como fornecer instâncias com @Provides
Podemos anotar uma função com @Provides
em módulos do Hilt para informar ao Hilt como fornecer tipos que não podem ser injetados pelo construtor.
O corpo da função com anotação @Provides
será executado sempre que o Hilt precisar fornecer uma instância desse tipo. O tipo de retorno da função com anotação @Provides
informa ao Hilt o tipo de vinculação ou como fornecer instâncias desse tipo. Os parâmetros da função são as dependências do tipo.
No nosso caso, incluiremos essa função na classe DatabaseModule
:
@Module
object DatabaseModule {
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
}
O código acima informa ao Hilt que database.logDao()
precisa ser executado ao fornecer uma instância de LogDao
. Como usamos AppDatabase
como uma dependência transitiva, também precisamos informar ao Hilt como fornecer instâncias desse tipo.
Como nosso projeto também não tem a classe AppDatabase
, que é gerada pela Room, podemos fornecê-la usando uma função @Provides
de uma forma parecida com a que criamos a instância do banco de dados na classe ServiceLocator
:
@Module
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
return Room.databaseBuilder(
appContext,
AppDatabase::class.java,
"logging.db"
).build()
}
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
}
Queremos que o Hilt sempre forneça a mesma instância de banco de dados, então anotaremos o método @Provides provideDatabase
com @Singleton
.
Cada contêiner do Hilt vem com um conjunto de vinculações padrão que podem ser injetadas como dependências nas suas vinculações personalizadas. Este é o caso do applicationContext
. Para acessá-lo, é necessário anotar o campo com @ApplicationContext
.
Como executar o app
O Hilt já tem todas as informações necessárias para injetar as instâncias em LogsFragment
, mas para funcionar corretamente, ele precisa estar ciente da Activity
que hospeda o Fragment
antes de executar o app. Para isso, é necessário anotar MainActivity
com @AndroidEntryPoint
.
Abra o arquivo ui/MainActivity.kt
e anote MainActivity
com @AndroidEntryPoint
:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() { ... }
Agora, você pode executar o app e verificar se tudo funciona como antes.
Vamos continuar refatorando o app para remover as chamadas ServiceLocator
da MainActivity
.
MainActivity
recebe uma instância do AppNavigator
do ServiceLocator
chamando a função provideNavigator(activity: FragmentActivity)
.
Como AppNavigator
é uma interface, não podemos usar a injeção de construtor. Para informar ao Hilt qual implementação usar para uma interface, você pode usar a anotação @Binds
em uma função dentro de um módulo do Hilt.
@Binds
precisa ser usada para anotar uma função abstrata. Como ela é abstrata, a função não contém nenhum código e a classe também precisará ser abstrata. O tipo de retorno da função abstrata é a interface para qual queremos fornecer uma implementação (por exemplo, AppNavigator
). A implementação é especificada adicionando um parâmetro exclusivo com o tipo de implementação da interface (por exemplo, AppNavigatorImpl
).
Podemos adicionar as informações à classe DatabaseModule
que criamos antes ou precisamos de um novo módulo? Há vários motivos para criarmos um novo módulo:
- Para uma organização melhor, o nome de um módulo precisa informar o tipo de informação que ele fornece. Por exemplo, não faz sentido incluir vinculações de navegação em um módulo com o nome
DatabaseModule
. - O módulo
DatabaseModule
é instalado noApplicationComponent
para que as vinculações estejam disponíveis no contêiner do aplicativo. Nossas novas informações de navegação (por exemplo,AppNavigator
) precisam de informações específicas da atividade (porqueAppNavigatorImpl
tem umaActivity
como dependência). Portanto, é preciso instalá-la no contêinerActivity
em vez do contêinerApplication
, já que é nele que as informações sobre aActivity
estão disponíveis. - Os módulos do Hilt não podem conter métodos de vinculação abstratos e não estáticos. Portanto, não é possível incluir anotações
@Binds
e@Provides
na mesma classe.
Crie um novo arquivo com o nome NavigationModule.kt
na pasta di
. Nela, vamos criar uma nova classe abstrata com o nome NavigationModule
e anotada com @Module
e @InstallIn(ActivityComponent::class)
, como explicado acima:
@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {
@Binds
abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}
Dentro do módulo, podemos adicionar a vinculação para AppNavigator
. Trata-se de uma função abstrata que retorna a interface que estamos informando ao Hilt, (por exemplo, AppNavigator
) e o parâmetro é a implementação dessa interface (por exemplo, AppNavigatorImpl
).
Agora, precisamos informar ao Hilt como fornecer instâncias de AppNavigatorImpl
. Como essa classe pode ser injetada pelo construtor, basta anotar o construtor dela com @Inject
.
Abra o arquivo navigator/AppNavigatorImpl.kt
e faça o seguinte:
class AppNavigatorImpl @Inject constructor(
private val activity: FragmentActivity
) : AppNavigator {
...
}
AppNavigatorImpl
depende de uma FragmentActivity
. Como uma instância AppNavigator
é fornecida no contêiner Activity
, FragmentActivity
já está disponível, já que é uma vinculação predefinida. A instância também está disponível em um contêiner Fragment
e em outro contêiner View
, já que NavigationModule
está instalado em ActivityComponent
.
Como usar o Hilt na atividade
Agora, o Hilt tem todas as informações para injetar uma instância AppNavigator
. Abra o arquivo MainActivity.kt
e faça o seguinte:
- Anote o campo
navigator
com@Inject
para usá-lo com o Hilt. - Remova o modificador de visibilidade
private
. - Remova o código de inicialização
navigator
na funçãoonCreate
.
O novo código ficará assim:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var navigator: AppNavigator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
navigator.navigateTo(Screens.BUTTONS)
}
}
...
}
Como executar o app
Execute o app e veja se ele está funcionando conforme o esperado.
Como concluir a refatoração
A única classe que ainda está usando o ServiceLocator
para capturar dependências é ButtonsFragment
. Como o Hilt já sabe como fornecer todos os tipos que ButtonsFragment
precisa, podemos simplesmente realizar a injeção de campo na classe.
Como visto anteriormente, para fazer com que a classe receba uma injeção de campo pelo Hilt, temos que:
- anotar o
ButtonsFragment
com@AndroidEntryPoint
; - remover o modificador particular dos campos
logger
enavigator
e anotá-los com@Inject
; - remover o código de inicialização dos campos (ou seja, os métodos
onAttach
epopulateFields
).
Código para ButtonsFragment
:
@AndroidEntryPoint
class ButtonsFragment : Fragment() {
@Inject lateinit var logger: LoggerLocalDataSource
@Inject lateinit var navigator: AppNavigator
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_buttons, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
}
}
A instância de LoggerLocalDataSource
será a mesma que usamos no LogsFragment
, já que o tipo tem como escopo o contêiner do aplicativo. No entanto, a instância do AppNavigator
será diferente da instância na MainActivity
, já que não definimos o escopo dela para o respectivo contêiner Activity
.
Neste ponto, a classe ServiceLocator
não fornece mais dependências, então podemos removê-la completamente do projeto. O único uso permanece na classe LogApplication
em que mantemos uma instância dela. Vamos limpar essa classe, já que não é mais necessária.
Abra a classe LogApplication
e remova o uso do ServiceLocator
. O novo código para a classe Application
será:
@HiltAndroidApp
class LogApplication : Application()
Agora, é possível remover completamente a classe ServiceLocator
do projeto. Como o ServiceLocator
ainda é usado em testes, remova também os usos dele da classe AppTest
.
Fim do conteúdo básico
Você acabou de aprender a usar o Hilt como a ferramenta de injeção de dependência no seu app Android.
Agora, adicionaremos novas funcionalidades ao nosso app para aprender a usar recursos mais avançados do Hilt em diferentes situações.
Depois de remover a classe ServiceLocator
do nosso projeto e aprender o básico sobre o Hilt, vamos adicionar novas funcionalidades ao app para explorar outros recursos do Hilt.
Nesta seção, você aprenderá:
- Como definir o escopo para o contêiner da atividade.
- O que são qualificadores, quais problemas eles resolvem e como usá-los.
Para demonstrar isso, precisamos de um comportamento diferente no app. Vamos substituir o armazenamento de registros de um banco de dados para uma lista na memória com a intenção de gravar apenas os registros feitos durante uma sessão do app.
Interface do LoggerDataSource
Vamos começar abstraindo a fonte de dados em uma interface. Crie um novo arquivo com o nome LoggerDataSource.kt
na pasta data
com o seguinte conteúdo:
package com.example.android.hilt.data
// Common interface for Logger data sources.
interface LoggerDataSource {
fun addLog(msg: String)
fun getAllLogs(callback: (List<Log>) -> Unit)
fun removeLogs()
}
LoggerLocalDataSource
é usado em ambos os fragmentos: ButtonsFragment
e LogsFragment
. É necessário refatorá-los para usá-los com uma instância de LoggerDataSource
.
Abra LogsFragment
e crie a variável de registro para o tipo LoggerDataSource
:
@AndroidEntryPoint
class LogsFragment : Fragment() {
@Inject lateinit var logger: LoggerDataSource
...
}
Faça o mesmo em ButtonsFragment
:
@AndroidEntryPoint
class ButtonsFragment : Fragment() {
@Inject lateinit var logger: LoggerDataSource
...
}
Agora, vamos fazer com que LoggerLocalDataSource
implemente essa interface. Abra o arquivo data/LoggerLocalDataSource.kt
e:
- Implemente a interface
LoggerDataSource
. - Marque os métodos com
override
.
@Singleton
class LoggerLocalDataSource @Inject constructor(
private val logDao: LogDao
) : LoggerDataSource {
...
override fun addLog(msg: String) { ... }
override fun getAllLogs(callback: (List<Log>) -> Unit) { ... }
override fun removeLogs() { ... }
}
Agora, vamos criar outra implementação de LoggerDataSource
com o nome LoggerInMemoryDataSource
que armazena os registros na memória. Crie um novo arquivo com o nome LoggerInMemoryDataSource.kt
na pasta data
com o seguinte conteúdo:
package com.example.android.hilt.data
import java.util.LinkedList
class LoggerInMemoryDataSource : LoggerDataSource {
private val logs = LinkedList<Log>()
override fun addLog(msg: String) {
logs.addFirst(Log(msg, System.currentTimeMillis()))
}
override fun getAllLogs(callback: (List<Log>) -> Unit) {
callback(logs)
}
override fun removeLogs() {
logs.clear()
}
}
Como definir o escopo para o contêiner da atividade
Para poder usar o LoggerInMemoryDataSource
como um detalhe de implementação, é necessário informar ao Hilt como fornecer instâncias desse tipo. Como visto anteriormente, anotamos o construtor da classe com @Inject
:
class LoggerInMemoryDataSource @Inject constructor(
) : LoggerDataSource { ... }
Como nosso aplicativo consiste em apenas uma atividade (também conhecido como um aplicativo de atividade única), é necessário ter uma instância de LoggerInMemoryDataSource
no contêiner da Activity
e reutilizá-la nos Fragment
s.
Para criar o comportamento de geração de registros na memória, defina o escopo de LoggerInMemoryDataSource
para o contêiner da Activity
. Cada Activity
criada terá o próprio contêiner, uma instância diferente. Em cada contêiner, a mesma instância de LoggerInMemoryDataSource
será fornecida quando ele for necessário como dependência ou para a injeção de campo. Além disso, a mesma instância será fornecida nos contêineres abaixo da hierarquia de componentes.
Seguindo a documentação do escopo dos componentes, para definir um tipo para o contêiner da Activity
, precisamos anotar o tipo com @ActivityScoped
:
@ActivityScoped
class LoggerInMemoryDataSource @Inject constructor(
) : LoggerDataSource { ... }
No momento, o Hilt sabe como fornecer instâncias de LoggerInMemoryDataSource
e LoggerLocalDataSource
, mas e quanto a LoggerDataSource
? O Hilt não sabe qual implementação usar quando o LoggerDataSource
for solicitado.
Como visto nas seções anteriores, podemos usar a anotação @Binds
em um módulo para informar ao Hilt qual implementação usar. No entanto, e se precisarmos fornecer ambas as implementações no mesmo projeto, como, por exemplo, usar LoggerInMemoryDataSource
enquanto o app estiver em execução e LoggerLocalDataSource
em um Service
?
Duas implementações para a mesma interface
Vamos criar um novo arquivo com o nome LoggingModule.kt
na pasta di
. Como as diferentes implementações de LoggerDataSource
têm escopo de diferentes contêineres, não podemos usar o mesmo módulo. LoggerInMemoryDataSource
tem o escopo definido para o contêiner Activity
, e LoggerLocalDataSource
tem o escopo definido para o contêiner Application
.
Felizmente, podemos definir vinculações para os dois módulos no mesmo arquivo que criamos:
package com.example.android.hilt.di
@InstallIn(ApplicationComponent::class)
@Module
abstract class LoggingDatabaseModule {
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
Os métodos @Binds
precisarão ter as anotações de escopo se o tipo tiver o escopo definido. Por isso, as funções acima são anotadas com @Singleton
e @ActivityScoped
. Se @Binds
ou @Provides
forem usados como vinculações para um tipo, as anotações de escopo no tipo não serão mais usadas. Portanto, você poderá removê-las das diferentes classes de implementação.
Se tentar criar o projeto agora, um erro DuplicateBindings
será exibido.
error: [Dagger/DuplicateBindings] com.example.android.hilt.data.LoggerDataSource is bound multiple times
Isso ocorre porque o tipo LoggerDataSource
está sendo injetado nos nossos Fragment
s, mas o Hilt não sabe qual implementação usar porque há duas vinculações do mesmo tipo. Como fazer com que o Hilt saiba qual usar?
Usar qualificadores
Para informar ao Hilt como fornecer implementações diferentes (várias vinculações) do mesmo tipo, você pode usarqualificadores.
É necessário definir um qualificador por implementação, já que cada qualificador será usado para identificar uma vinculação. Ao injetar o tipo em uma classe Android ou usar esse tipo como uma dependência de outras classes, a anotação do qualificador precisa ser usada para evitar ambiguidade.
Como um qualificador é apenas uma anotação, podemos defini-lo no arquivo LoggingModule.kt
em que adicionamos os módulos:
package com.example.android.hilt.di
@Qualifier
annotation class InMemoryLogger
@Qualifier
annotation class DatabaseLogger
Agora, esses qualificadores precisam ser usados para anotar as funções @Binds
(ou @Provides
, caso seja necessário) que forneçam cada implementação. Veja o código completo e observe o uso de qualificadores nos métodos @Binds
:
package com.example.android.hilt.di
@Qualifier
annotation class InMemoryLogger
@Qualifier
annotation class DatabaseLogger
@InstallIn(ApplicationComponent::class)
@Module
abstract class LoggingDatabaseModule {
@DatabaseLogger
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@InMemoryLogger
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
Além disso, esses qualificadores precisam ser usados no ponto de injeção com a implementação que queremos injetar. Neste caso, vamos usar a implementação de LoggerInMemoryDataSource
nos Fragment
s.
Abra LogsFragment
e use o qualificador @InMemoryLogger
no campo de registro para instruir o Hilt a injetar uma instância de LoggerInMemoryDataSource
:
@AndroidEntryPoint
class LogsFragment : Fragment() {
@InMemoryLogger
@Inject lateinit var logger: LoggerDataSource
...
}
Faça o mesmo para ButtonsFragment
:
@AndroidEntryPoint
class ButtonsFragment : Fragment() {
@InMemoryLogger
@Inject lateinit var logger: LoggerDataSource
...
}
Se quiser alterar a implementação do banco de dados que quer usar, basta anotar os campos injetados com @DatabaseLogger
em vez de @InMemoryLogger
.
Executar o app
Podemos executar o app e confirmar o que fizemos usando os botões e observando se os registros adequados são exibidos na tela "See all logs".
Observe que os registros não são mais salvos no banco de dados. Eles não são mantidos entre as sessões, então sempre que você fechar e abrir o app novamente, a tela de registro estará vazia.
Agora que o app foi totalmente migrado para o Hilt, também é possível migrar o teste de instrumentação usado no projeto. O teste que verifica a funcionalidade do app está no arquivo AppTest.kt
na pasta app/androidTest
. Abra ele.
Você verá que ele não pode ser compilado porque removemos a classe ServiceLocator
do projeto. Remova as referências ao ServiceLocator
que não estamos mais usando removendo o método @After tearDown
da classe.
Os testes androitTest
são executados em um emulador. O teste happyPath
confirma que o toque no "Button 1" foi registrado no banco de dados. Como o app está usando o banco de dados na memória, após o término do teste, todos os registros desaparecerão.
Como realizar testes de IU com o Hilt
O Hilt injeta dependências no teste da IU como aconteceria no código de produção.
Os testes com o Hilt não precisam de manutenção porque geram automaticamente um novo conjunto de componentes para cada teste.
Como adicionar dependências de teste
O Hilt usa uma biblioteca adicional com anotações específicas para testes com o nome hilt-android-testing
que facilitam o teste do código e que precisa ser adicionada ao projeto. Além disso, como o Hilt precisa gerar o código para classes na pasta androidTest
, o processador de anotações também precisa ser executado na pasta. Para ativar esse recurso, inclua duas dependências no arquivo app/build.gradle
.
Para adicionar essas dependências, abra app/build.gradle
e adicione esta configuração à parte inferior da seção dependencies
:
...
dependencies {
// Hilt testing dependency
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
// Make Hilt generate code in the androidTest folder
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
}
TestRunner personalizado
Os testes instrumentados que usam o Hilt precisam ser executados em um Application
compatível com o Hilt. A biblioteca já vem com um HiltTestApplication
que podemos usar para executar nossos testes de IU. Para especificar o Application
a ser usado nos testes, crie um novo executor de teste no projeto.
No mesmo nível, o arquivo AppTest.kt
está na pasta androidTest
. Crie um novo arquivo com o nome CustomTestRunner
. Nosso CustomTestRunner
é estendido do AndroidJUnitRunner e é implementado da seguinte forma:
class CustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
Em seguida, é necessário instruir o projeto a usar esse executor de testes para realizar os testes de instrumentação. Especifique isso no atributo testInstrumentationRunner
do arquivo app/build.gradle
. Abra o arquivo e substitua o conteúdo padrão testInstrumentationRunner
pelo seguinte:
...
android {
...
defaultConfig {
...
testInstrumentationRunner "com.example.android.hilt.CustomTestRunner"
}
...
}
...
Estamos prontos para usar o Hilt em nossos testes de IU.
Como executar um teste usando o Hilt
Para que uma classe de teste do emulador use o Hilt, ela precisa:
- ser anotada com
@HiltAndroidTest
, responsável por gerar os componentes Hilt para cada teste; - usar a
HiltAndroidRule
, que gerencia o estado dos componentes e é usada para realizar a injeção no teste.
Vamos incluí-las no AppTest
:
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
class AppTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
...
}
Agora você pode executar o teste usando o botão ao lado da definição da classe ou do método de teste. Se você tiver configurado um emulador, ele será iniciado e o teste será aprovado.
Para saber mais sobre testes e recursos, como a injeção de campo ou a substituição de vinculações nos testes, consulte a documentação.
Nesta seção do codelab, você aprenderá a usar a anotação @EntryPoint
, que é usada para injetar dependências em classes não compatíveis com o Hilt.
Como vimos anteriormente, o Hilt é compatível com os componentes do Android mais comuns. No entanto, talvez seja necessário realizar a injeção de campo em classes que não são diretamente compatíveis com o Hilt ou não podem usá-lo.
Nesses casos, é possível usar @EntryPoint
. Um ponto de entrada é o limite do local onde você pode acessar objetos fornecidos pelo Hilt pelo código que não pode usar o Hilt para injetar as dependências dele. É o ponto em que o código entra pela primeira vez nos contêineres gerenciados pelo Hilt.
O caso de uso
Queremos exportar nossos registros para fora do processo do aplicativo. Para isso, precisamos usar um ContentProvider
. Só permitimos que os clientes consultem um registro específico (fornecendo um id
) ou todos os registros do app usando um ContentProvider
. Usaremos este banco de dados para recuperar os dados. Dessa forma, a classe LogDao
precisa expor métodos que retornam as informações necessárias usando um banco de dados Cursor
. Abra o arquivo LogDao.kt
e adicione os seguintes métodos à interface.
@Dao
interface LogDao {
...
@Query("SELECT * FROM logs ORDER BY id DESC")
fun selectAllLogsCursor(): Cursor
@Query("SELECT * FROM logs WHERE id = :id")
fun selectLogById(id: Long): Cursor?
}
Em seguida, precisamos criar uma nova classe ContentProvider
e substituir o método query
para retornar um Cursor
com os registros. Crie um novo arquivo com o nome LogsContentProvider.kt
em um novo diretório contentprovider
com o seguinte conteúdo:
package com.example.android.hilt.contentprovider
import android.content.ContentProvider
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.content.UriMatcher
import android.database.Cursor
import android.net.Uri
import com.example.android.hilt.data.LogDao
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ApplicationComponent
import java.lang.UnsupportedOperationException
/** The authority of this content provider. */
private const val LOGS_TABLE = "logs"
/** The authority of this content provider. */
private const val AUTHORITY = "com.example.android.hilt.provider"
/** The match code for some items in the Logs table. */
private const val CODE_LOGS_DIR = 1
/** The match code for an item in the Logs table. */
private const val CODE_LOGS_ITEM = 2
/**
* A ContentProvider that exposes the logs outside the application process.
*/
class LogsContentProvider: ContentProvider() {
private val matcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, LOGS_TABLE, CODE_LOGS_DIR)
addURI(AUTHORITY, "$LOGS_TABLE/*", CODE_LOGS_ITEM)
}
override fun onCreate(): Boolean {
return true
}
/**
* Queries all the logs or an individual log from the logs database.
*
* For the sake of this codelab, the logic has been simplified.
*/
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
val code: Int = matcher.match(uri)
return if (code == CODE_LOGS_DIR || code == CODE_LOGS_ITEM) {
val appContext = context?.applicationContext ?: throw IllegalStateException()
val logDao: LogDao = getLogDao(appContext)
val cursor: Cursor? = if (code == CODE_LOGS_DIR) {
logDao.selectAllLogsCursor()
} else {
logDao.selectLogById(ContentUris.parseId(uri))
}
cursor?.setNotificationUri(appContext.contentResolver, uri)
cursor
} else {
throw IllegalArgumentException("Unknown URI: $uri")
}
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
throw UnsupportedOperationException("Only reading operations are allowed")
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
throw UnsupportedOperationException("Only reading operations are allowed")
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
throw UnsupportedOperationException("Only reading operations are allowed")
}
override fun getType(uri: Uri): String? {
throw UnsupportedOperationException("Only reading operations are allowed")
}
}
Você verá que a chamada getLogDao(appContext)
não pode ser compilada. Precisamos implementá-la capturando a dependência LogDao
do contêiner do aplicativo do Hilt, mas o Hilt não é naturalmente compatível com a injeção de um ContentProvider
. Isso acontece ao injetar @AndroidEntryPoint
com uma atividade, por exemplo.
Precisamos criar uma nova interface anotada com @EntryPoint
para acessá-la.
@EntryPoint em ação
Um ponto de entrada é uma interface com um método do acessador para cada tipo de vinculação que queremos usar (incluindo o qualificador). Além disso, a interface precisa ser anotada com @InstallIn
para especificar o componente em que o ponto de entrada precisa ser instalado.
A prática recomendada é adicionar a nova interface de ponto de entrada na classe que a utiliza. Sendo assim, inclua a interface no arquivo LogsContentProvider.kt
:
class LogsContentProvider: ContentProvider() {
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface LogsContentProviderEntryPoint {
fun logDao(): LogDao
}
...
}
Observe que a interface é anotada com @EntryPoint
e é instalada no ApplicationComponent
, já que queremos usar a dependência de uma instância do contêiner Application
. Na interface, métodos são expostos para as vinculações que queremos acessar, nesse caso, LogDao
.
Para acessar um ponto de entrada, use o método estático adequado dos EntryPointAccessors
. O parâmetro precisa ser a instância do componente ou o objeto @AndroidEntryPoint
que atua como detentor do componente. Verifique se o componente transmitido como parâmetro e o método estático EntryPointAccessors
correspondem à classe Android na anotação @InstallIn
na interface @EntryPoint
:
Agora, podemos implementar o método getLogDao
que está faltando no código acima. Vamos usar a interface de ponto de entrada que definimos acima na nossa classe LogsContentProviderEntryPoint
:
class LogsContentProvider: ContentProvider() {
...
private fun getLogDao(appContext: Context): LogDao {
val hiltEntryPoint = EntryPointAccessors.fromApplication(
appContext,
LogsContentProviderEntryPoint::class.java
)
return hiltEntryPoint.logDao()
}
}
Observe a forma como transmitimos o applicationContext
para o método estático EntryPoints.get
e a classe da interface que é anotada com @EntryPoint
.
Agora você já sabe como usar o Hilt e poderá adicioná-lo ao seu app Android. Veja o que você aprendeu neste codelab:
- Como configurar o Hilt na classe do seu aplicativo usando
@HiltAndroidApp
. - Como adicionar contêineres de dependência a diferentes componentes do ciclo de vida do Android usando
@AndroidEntryPoint
. - Como usar módulos para que o Hilt saiba como fornecer determinados tipos.
- Como usar qualificadores para fornecer várias vinculações para determinados tipos.
- Como testar seu app usando o Hilt.
- Quando o
@EntryPoint
é útil e como usá-lo.