Neste codelab, você aprenderá a migrar o Dagger para o Hilt para injeção de dependência (DI) em um app Android. Ele faz a migração do seu app Android com Dagger para o Hilt. O objetivo do codelab é mostrar como planejar a migração e manter o Dagger e o Hilt trabalhando lado a lado durante o processo. O app continua funcional enquanto você migra cada componente do Dagger para o Hilt.
A injeção de dependência ajuda na reutilização de código e na facilidade de refatoração e de testes. O Hilt foi criado com base na conhecida biblioteca de DI Dagger. Ele se beneficia com a precisão do tempo de compilação, o desempenho no ambiente de execução, a escalonabilidade e a compatibilidade com o Android Studio oferecidos pelo Dagger.
Como muitas classes do framework do Android são instanciadas pelo próprio sistema operacional, há um código clichê associado ao uso do Dagger em apps Android. O Hilt remove a maior parte desse código clichê gerando e fornecendo automaticamente:
- componentes para integrar classes de framework do Android com o Dagger, que precisariam ser criados manualmente sem o Hilt;
- anotações de escopo para os componentes que o Hilt gera automaticamente;
- vinculações e qualificadores predefinidos.
O melhor de tudo é que, como o Dagger e o Hilt podem coexistir, os apps podem ser migrados conforme necessário.
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.
Pré-requisitos
- Experiência com a sintaxe do Kotlin
- Experiência com o Dagger
O que você aprenderá
- Como adicionar o Hilt ao seu app Android.
- Como planejar uma estratégia de migração.
- Como migrar componentes para o Hilt e manter o código do Dagger funcionando.
- Como migrar componentes com escopo.
- Como testar seu app usando o Hilt.
Pré-requisitos
- Android Studio 4.0 ou uma versão mais recente
Buscar o código
Consiga o código do codelab no GitHub:
$ git clone https://github.com/googlecodelabs/android-dagger-to-hilt
Se preferir, faça o download do repositório como um arquivo ZIP:
Abrir o Android Studio
Se precisar, faça o download do Android Studio aqui.
Configuração do projeto
O projeto é criado em várias ramificações do GitHub:
master
é a ramificação consultada ou transferida por download. Ela é o ponto de partida do codelab.interop
é a ramificação de interoperabilidade do Dagger e do Hilt.solution
contém a solução para este codelab, incluindo testes e ViewModels.
Recomendamos que você siga o codelab passo a passo no seu próprio ritmo, começando pela ramificação master
.
Durante o codelab, você verá snippets de código que precisará adicionar ao projeto. Em alguns locais, também será necessário remover o código que será explicitamente mencionado nos comentários dos snippets de código.
Como checkpoints, você tem as ramificações intermediárias disponíveis caso precise de ajuda com uma etapa específica.
Para conseguir a ramificação solution
pelo git, use o seguinte comando:
$ git clone -b solution https://github.com/googlecodelabs/android-dagger-to-hilt
Ou faça o download do código da solução aqui:
Perguntas frequentes
Como executar o app de exemplo
Primeiro, vamos ver a aparência do app de exemplo inicial. Siga estas instruções para abrir o app de exemplo no Android Studio.
- Se você fez o download do arquivo ZIP, descompacte-o localmente.
- Abra o projeto no Android Studio.
- Clique no botão Run
e escolha um emulador ou conecte seu dispositivo Android. A tela de registro será exibida.
O app consiste em quatro fluxos diferentes que funcionam com o Dagger (implementados como atividades):
- Registro: o usuário pode se registrar inserindo nome de usuário e senha e aceitando nossos Termos e Condições.
- Login: o usuário pode fazer login usando as credenciais adicionadas durante o fluxo de registro e também pode cancelar a inscrição no app.
- Início: o usuário recebe as boas-vindas e pode ver quantas notificações não lidas ele tem.
- Configurações: o usuário pode sair e atualizar o número de notificações não lidas, o que produz um número aleatório de notificações.
O projeto segue um padrão MVVM típico, em que toda a complexidade da visualização é adiada para um ViewModel. Familiarize-se com a estrutura do projeto.
As setas representam dependências entre objetos. É isso que chamamos de gráfico de aplicativo: todas as classes do app e as dependências entre elas.
O código na ramificação master
usa o Dagger para injetar dependências. Em vez de criar componentes manualmente, vamos refatorar o app para que use o Hilt para gerar componentes e outros códigos relacionados ao Dagger.
O Dagger é configurado no app conforme mostrado no diagrama a seguir. O ponto em certos tipos significa que o tipo é limitado ao componente que o fornece:
Para facilitar, as dependências do Hilt já foram adicionadas a este projeto na ramificação master
transferida por download inicialmente. Não é necessário adicionar o código a seguir ao seu projeto, porque 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 que é configurado no projeto. Abra o arquivo raiz build.gradle
(no nível do projeto) e encontre 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"
}
}
Abra app/build.gradle
e consulte a declaração do plug-in do Hilt para Gradle na parte superior, abaixo do plug-in kotlin-kapt
.
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
}
Por fim, as dependências do Hilt e o processador de anotações estão incluídos 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 a Hilt, são transferidas por download durante a criação e a sincronização do projeto. Vamos começar a usar o Hilt!
Pode ser tentador migrar todos os itens para o Hilt de uma vez. Porém, em um projeto real, é melhor que o app seja criado e funcione sem erros durante a migração para o Hilt em etapas.
Ao migrar para o Hilt, organize seu trabalho em etapas. A abordagem recomendada é começar pela migração do componente Application ou @Singleton
e depois migrar as atividades e os fragmentos.
No codelab, você migrará o AppComponent
primeiro e depois cada um dos fluxos do app, começando pelo Registro, seguido pelo Login, Principal e Configurações.
Durante a migração, você removerá todas as interfaces @Component
e @Subcomponent
e anotará todos os módulos com @InstallIn
.
Após a migração, todas as classes Application
/Activity
/Fragment
/View
/Service
/BroadcastReceiver
deverão ser anotadas com @AndroidEntryPoint
. Todo código que instancie ou propague componentes também precisará ser removido.
Para planejar a migração, vamos começar pelo AppComponent.kt
para entender a hierarquia de componentes.
@Singleton
// Definition of a Dagger component that adds info from the different modules to the graph
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
// Factory to create instances of the AppComponent
@Component.Factory
interface Factory {
// With @BindsInstance, the Context passed in will be available in the graph
fun create(@BindsInstance context: Context): AppComponent
}
// Types that can be retrieved from the graph
fun registrationComponent(): RegistrationComponent.Factory
fun loginComponent(): LoginComponent.Factory
fun userManager(): UserManager
}
O AppComponent
é anotado com @Component
e inclui dois módulos, StorageModule
e AppSubcomponents
.
O AppSubcomponents
tem três componentes, RegistrationComponent
, LoginComponent
e UserComponent
.
- O
LoginComponent
é injetado naLoginActivity
- O
RegistrationComponent
é injetado naRegistrationActivity
, noEnterDetailsFragment
e noTermsAndConditionsFragment
. Esse componente também está no escopo daRegistrationActivity
.
O UserComponent é injetado na MainActivity
e na SettingsActivity
.
As referências ao ApplicationComponent
podem ser substituídas pelo componente gerado pelo Hilt (link para todos os componentes gerados), que é mapeado para o componente que você está migrando no seu app.
Nesta seção, você migrará o AppComponent
. É necessário preparar o terreno para manter o código do Dagger funcionando enquanto você migra cada componente para o Hilt nas próximas etapas.
Para inicializar o Hilt e iniciar a geração de código, é preciso fazer anotações do Hilt na sua classe Application
.
Abra MyApplication.kt
e adicione a anotação @HiltAndroidApp
à classe. Essas anotações fazem com que o Hilt acione a geração de código que o Dagger usará no processador de anotações.
MyApplication.kt
package com.example.android.dagger
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
open class MyApplication : Application() {
// Instance of the AppComponent that will be used by all the Activities in the project
val appComponent: AppComponent by lazy {
initializeComponent()
}
open fun initializeComponent(): AppComponent {
// Creates an instance of AppComponent using its Factory constructor
// We pass the applicationContext that will be used as Context in the graph
return DaggerAppComponent.factory().create(applicationContext)
}
}
1. Migrar módulos de componente
Para começar, abra AppComponent.kt. O AppComponent
tem dois módulos (StorageModule
e AppSubcomponents
) adicionados na anotação @Component
. A primeira coisa que você precisa fazer é migrar esses dois módulos para que o Hilt os adicione ao ApplicationComponent
gerado.
Para fazer isso, abra AppSubcomponents.kt e faça a anotação @InstallIn
na classe. A anotação @InstallIn
usa um parâmetro para adicionar o módulo ao componente certo. Nesse caso, ao migrar o componente no nível do app, é importante que as vinculações sejam geradas em ApplicationComponent
.
AppSubcomponents.kt
// This module tells a Component which are its subcomponents
// Install this module in Hilt-generated ApplicationComponent
@InstallIn(ApplicationComponent::class)
@Module(
subcomponents = [
RegistrationComponent::class,
LoginComponent::class,
UserComponent::class
]
)
class AppSubcomponents
É preciso fazer a mesma mudança em StorageModule
. Abra StorageModule.kt e adicione a anotação @InstallIn
, como fez na etapa anterior.
StorageModule.kt
// Tells Dagger this is a Dagger module
// Install this module in Hilt-generated ApplicationComponent
@InstallIn(ApplicationComponent::class)
@Module
abstract class StorageModule {
// Makes Dagger provide SharedPreferencesStorage when a Storage type is requested
@Binds
abstract fun provideStorage(storage: SharedPreferencesStorage): Storage
}
Com a anotação @InstallIn
, você pediu novamente ao Hilt para adicionar o módulo ao ApplicationComponent gerado pelo Hilt.
Agora, vamos voltar e conferir AppComponent.kt. O AppComponent
fornece dependências para o RegistrationComponent
, o LoginComponent
e o UserManager
. Nas próximas etapas, você preparará esses componentes para a migração.
2. Migrar tipos expostos
Ao migrar o app inteiro para o Hilt, você pode solicitar dependências do Dagger manualmente usando pontos de entrada. Com os pontos de entrada, você mantém o app funcionando enquanto migra os componentes do Dagger. Nesta etapa, você substituirá cada componente do Dagger por uma consulta de dependência manual no ApplicationComponent
gerado pelo Hilt.
Para conseguir o RegistrationComponent.Factory
para RegistrationActivity.kt
usando o ApplicationComponent
gerado pelo Hilt, você precisa criar uma nova interface EntryPoint anotada com @InstallIn
. A anotação InstallIn
instrui o Hilt onde buscar a vinculação. Para acessar um ponto de entrada, use o método estático adequado de EntryPointAccessors
. O parâmetro precisa ser a instância do componente ou o objeto @AndroidEntryPoint
que atua como o detentor do componente.
RegistrationActivity.kt
class RegistrationActivity : AppCompatActivity() {
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface RegistrationEntryPoint {
fun registrationComponent(): RegistrationComponent.Factory
}
...
}
Agora, você precisa substituir o código relacionado ao Dagger pelo RegistrationEntryPoint
. Mude a inicialização do registrationComponent
para usar o RegistrationEntryPoint
. Com essa mudança, a RegistrationActivity
poderá acessar as dependências no código gerado pelo Hilt até ser migrada para ele.
RegistrationActivity.kt
// Creates an instance of Registration component by grabbing the factory from the app graph
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, RegistrationEntryPoint::class.java)
registrationComponent = entryPoint.registrationComponent().create()
A seguir, você precisa fazer o mesmo trabalho de preparação para todos os outros tipos de componentes expostos. Vamos continuar com o LoginComponent.Factory
. Abra a LoginActivity
e crie uma interface LoginEntryPoint
anotada com @InstallIn
e @EntryPoint
, como você fez anteriormente, mas expondo o que a LoginActivity
precisa do componente Hilt.
LoginActivity.kt
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface LoginEntryPoint {
fun loginComponent(): LoginComponent.Factory
}
Agora que o Hilt sabe como fornecer o LoginComponent
, substitua a antiga chamada inject()
pelo loginComponent()
do EntryPoint.
LoginActivity.kt
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, LoginEntryPoint::class.java)
entryPoint.loginComponent().create().inject(this)
Dois dos três tipos expostos do AppComponent
são substituídos para funcionar com EntryPoints do Hilt. Em seguida, faça uma modificação semelhante para o UserManager
. Ao contrário do RegistrationComponent
e do LoginComponent
, o UserManager
é usado na MainActivity
e na SettingsActivity
. Você só precisa criar uma interface EntryPoint uma vez. Essa interface anotada pode ser usada nas duas atividades. Para simplificar, declare a interface na MainActivity.
Para criar uma interface UserManagerEntryPoint
, abra MainActivity.kt
e faça uma anotação com @InstallIn
e @EntryPoint
.
MainActivity.kt
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface UserManagerEntryPoint {
fun userManager(): UserManager
}
Agora, mude o UserManager
para que ele use o UserManagerEntryPoint
.
MainActivity.kt
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, UserManagerEntryPoint::class.java)
val userManager = entryPoint.userManager()
É preciso fazer a mesma mudança na SettingsActivity.
. Abra SettingsActivity.kt
e mude a forma como o UserManager
é injetado.
SettingsActivity.kt
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, MainActivity.UserManagerEntryPoint::class.java)
val userManager = entryPoint.userManager()
3. Remover fábrica de componente
Fazer a transmissão do Context
para um componente Dagger usando @BindsInstance
é um padrão comum. Isso não é necessário no Hilt, porque Context
já está disponível como uma vinculação predefinida.
O Context
normalmente é necessário para acessar recursos, bancos de dados, preferências compartilhadas etc. O Hilt simplifica a injeção de contexto usando os qualificadores @ApplicationContext
e @ActivityContext
.
Ao migrar seu app, verifique quais tipos precisam do Context
como dependência e substitua-os pelos fornecidos pelo Hilt.
Nesse caso, o SharedPreferencesStorage
tem Context
como dependência. Para instruir o Hilt a injetar o contexto, abra SharedPreferencesStorage.kt. SharedPreferences
. precisa do Context
do app. Portanto, adicione a anotação @ApplicationContext
ao parâmetro de contexto.
SharedPreferencesStorage.kt
class SharedPreferencesStorage @Inject constructor(
@ApplicationContext context: Context
) : Storage {
//...
4. Migrar métodos de injeção
Em seguida, é necessário consultar o código do componente para os métodos inject()
e anotar as classes correspondentes com @AndroidEntryPoint
. No nosso caso, o AppComponent
não tem métodos inject()
. Por isso, não é preciso fazer nada.
5. Remover a classe AppComponent
Como você já adicionou EntryPoints para todos os componentes listados em AppComponent.kt
, exclua AppComponent.kt
.
6. Remover o código que usa o Component para migração
Você não precisa mais do código para inicializar o AppComponent
personalizado na classe do app. Em vez disso, a classe Application usa o ApplicationComponent gerado pelo Hilt. Remova todo o código do corpo da classe. O código final será algo como a lista de código abaixo.
MyApplication.kt
package com.example.android.dagger
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
open class MyApplication : Application()
Com isso, você adicionou o Hilt ao seu app, removeu o AppComponent
e modificou o código do Dagger para injetar dependências sobre o AppComponent gerado pelo Hilt. Quando você compila e testa o app em um dispositivo ou emulador, o app precisa funcionar exatamente como antes. Nas próximas seções, migraremos atividades e fragmentos para o Hilt.
Agora que você migrou o componente Application e preparou uma base, é possível migrar cada componente para o Hilt individualmente.
Vamos começar migrando o fluxo de login. Em vez de criar o LoginComponent
manualmente e usá-lo na LoginActivity
, é possível pedir que o Hilt faça isso para você.
Você pode seguir as mesmas etapas usadas na seção anterior, mas, desta vez, use o ActivityComponent
gerado pelo Hilt, porque vamos migrar um componente gerenciado por uma atividade.
Para começar, abra LoginComponent.kt. O LoginComponent
não tem módulos, então, não é necessário fazer nada. Para que o Hilt gere um componente para a LoginActivity
e faça a injeção, você precisa anotar a atividade com @AndroidEntryPoint
.
LoginActivity.kt
@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {
//...
}
Você precisa adicionar apenas esse código para migrar a LoginActivity
para o Hilt. Como o Hilt vai gerar o código relacionado ao Dagger, basta fazer a limpeza dele. Exclua a interface LoginEntryPoint
.
LoginActivity.kt
//Remove
//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface LoginEntryPoint {
// fun loginComponent(): LoginComponent.Factory
//}
Em seguida, remova o código EntryPoint em onCreate()
.
LoginActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
//Remove
//val entryPoint = EntryPoints.get(applicationContext, LoginActivity.LoginEntryPoint::class.java)
//entryPoint.loginComponent().create().inject(this)
super.onCreate(savedInstanceState)
...
}
Como o Hilt gerará o componente, localize e exclua LoginComponent.kt.
O LoginComponent
está listado como um subcomponente em AppSubcomponents.kt. É possível excluir o LoginComponent
com segurança da lista de subcomponentes porque o Hilt gerará as vinculações para você.
AppSubcomponents.kt
// This module tells a Component which are its subcomponents
@InstallIn(ApplicationComponent::class)
@Module(
subcomponents = [
RegistrationComponent::class,
UserComponent::class
]
)
class AppSubcomponents
Isso é tudo que você precisa para migrar a LoginActivity
para o Hilt. Nesta seção, você excluiu muito mais código do que adicionou, o que é ótimo. Ao usar o Hilt, você digita menos código. Com isso, também há menos código para manter e menos bugs.
Nesta seção, você migrará o fluxo de registro. Para planejar a migração, vamos dar uma olhada em RegistrationComponent
. Abra RegistrationComponent.kt e role para baixo até a função inject(). O RegistrationComponent
é responsável por injetar dependências para RegistrationActivity
, EnterDetailsFragment
e TermsAndConditionsFragment
.
Vamos começar pela migração da RegistrationActivity
. Abra RegistrationActivity.kt e anote a classe com @AndroidEntryPoint
.
RegistrationActivity.kt
@AndroidEntryPoint
class RegistrationActivity : AppCompatActivity() {
//...
}
Agora que RegistrationActivity
está registrado no Hilt, você pode remover a interface RegistrationEntryPoint
e o código relacionado ao EntryPoint da função onCreate()
.
RegistrationActivity.kt
//Remove
//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface RegistrationEntryPoint {
// fun registrationComponent(): RegistrationComponent.Factory
//}
override fun onCreate(savedInstanceState: Bundle?) {
//Remove
//val entryPoint = EntryPoints.get(applicationContext, RegistrationEntryPoint::class.java)
//registrationComponent = entryPoint.registrationComponent().create()
registrationComponent.inject(this)
super.onCreate(savedInstanceState)
//..
}
O Hilt é responsável por gerar o componente e injetar dependências para que você possa remover a variável registrationComponent
e a chamada de injeção no componente Dagger excluído.
RegistrationActivity.kt
// Remove
// lateinit var registrationComponent: RegistrationComponent
override fun onCreate(savedInstanceState: Bundle?) {
//Remove
//registrationComponent.inject(this)
super.onCreate(savedInstanceState)
//..
}
Em seguida, abra EnterDetailsFragment.kt. Anote o EnterDetailsFragment
com @AndroidEntryPoint
, como você fez na RegistrationActivity
.
EnterDetailsFragment.kt
@AndroidEntryPoint
class EnterDetailsFragment : Fragment() {
//...
}
Como o Hilt fornece as dependências, não é necessário chamar inject()
no componente do Dagger excluído. Exclua a função onAttach()
.
A próxima etapa é migrar o TermsAndConditionsFragment
. Abra TermsAndConditionsFragment.kt, anote a classe e remova a função onAttach()
, como fez na etapa anterior. O código final ficará assim.
TermsAndConditionsFragment.kt
@AndroidEntryPoint
class TermsAndConditionsFragment : Fragment() {
@Inject
lateinit var registrationViewModel: RegistrationViewModel
//override fun onAttach(context: Context) {
// super.onAttach(context)
//
// // Grabs the registrationComponent from the Activity and injects this Fragment
// (activity as RegistrationActivity).registrationComponent.inject(this)
//}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_terms_and_conditions, container, false)
view.findViewById<Button>(R.id.next).setOnClickListener {
registrationViewModel.acceptTCs()
(activity as RegistrationActivity).onTermsAndConditionsAccepted()
}
return view
}
}
Com essa mudança, você migrou todos os fragmentos e atividades listados no RegistrationComponent
para excluir RegistrationComponent.kt.
Depois de excluir o RegistrationComponent
, é necessário remover a referência dele da lista de subcomponentes em AppSubcomponents
.
AppSubcomponents.kt
@InstallIn(ApplicationComponent::class)
// This module tells a Component which are its subcomponents
@Module(
subcomponents = [
UserComponent::class
]
)
class AppSubcomponents
Falta apenas uma etapa para concluir a migração do fluxo de registro. Esse fluxo declara e usa o próprio escopo, ActivityScope
. Os escopos controlam o ciclo de vida das dependências. Nesse caso, o ActivityScope
instrui o Dagger a injetar a mesma instância de RegistrationViewModel
no fluxo iniciado com a RegistrationActivity
. O Hilt oferece escopos de ciclo de vida integrados para que haja compatibilidade com esse processo.
Abra o RegistrationViewModel
e mude a anotação @ActivityScope
com o @ActivityScoped
fornecido pelo Hilt.
RegistrationViewModel.kt
@ActivityScoped
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
//...
}
Como o ActivityScope
não é usado em nenhum outro lugar, você pode excluir o ActivityScope.kt com segurança.
Agora, execute o app e teste o fluxo de registro. Use seu nome de usuário e senha atuais para fazer login ou cancelar a inscrição. Registre-se novamente com uma nova conta para confirmar que o fluxo funciona exatamente como antes.
No momento, o Dagger e o Hilt estão trabalhando juntos no app. O Hilt está injetando todas as dependências, exceto UserManager
. Na próxima seção, você migrará completamente do Dagger para o Hilt com a migração do UserManager
.
Até aqui neste codelab, você migrou a maior parte do app de exemplo para o Hilt, exceto um componente, UserComponent
. O UserComponent
é anotado com um escopo personalizado, @LoggedUserScope
. Isso significa que o UserComponent
injetará a mesma instância do UserManager
nas classes anotadas com @LoggedUserScope
.
O UserComponent
não mapeia nenhum dos componentes do Hilt disponíveis, já que o ciclo de vida dele não é gerenciado por uma classe do Android. Como não é possível adicionar um componente personalizado no meio da hierarquia gerada pelo Hilt, você tem duas opções:
- Deixar o Hilt e o Dagger lado a lado no estado em que o projeto está.
- Migrar o componente com escopo para o componente do Hilt mais próximo (
ApplicationComponent
, neste caso) e usar nulidade quando necessário.
Você já realizou a primeira opção na etapa anterior. Nesta etapa, você fará a segunda para que o app seja totalmente migrado para o Hilt. No entanto, em um app real, você pode escolher a opção que melhor se adapta ao seu caso de uso específico.
Nesta etapa, o UserComponent
será migrado para fazer parte do ApplicationComponent
do Hilt. Se houver módulos nesse componente, eles também deverão ser instalados no ApplicationComponent
.
O único tipo com escopo no UserComponent
é UserDataRepository
, anotado com @LoggedUserScope
. Como o UserComponent
terá convergência com o ApplicationComponent
do Hilt, o UserDataRepository
será anotado com @Singleton
e você mudará a lógica para anulá-la quando o usuário estiver desconectado.
O UserManager
já está anotado com @Singleton
, o que significa que você pode fornecer a mesma instância em todo o app. Com algumas mudanças, você poderá ter a mesma funcionalidade com o Hilt. Vamos começar modificando a forma como o UserManager
e o UserDataRepository
funcionam, porque primeiro você precisa de uma base.
Abra UserManager.kt
e aplique as modificações a seguir.
- Substitua o parâmetro
UserComponent.Factory
peloUserDataRepository
no construtor, porque não é mais necessário criar uma instância doUserComponent
. Ele tem oUserDataRepository
como uma dependência. - Como o Hilt gerará o código do componente, exclua o
UserComponent
e o setter dele. - Mude a função
isUserLoggedIn()
para conferir o nome de usuário douserRepository
, em vez douserComponent
. - Adicione o nome de usuário como um parâmetro à função
userJustLoggedIn()
. - Mude o corpo da função
userJustLoggedIn()
para chamarinitData
comuserName
nouserDataRepository
, em vez deuserComponent
, que você excluirá durante a migração. - Adicione o
username
à chamadauserJustLoggedIn()
nas funçõesregisterUser()
eloginUser()
. - Remova o
userComponent
da funçãologout()
e substitua-o por uma chamada parauserDataRepository.cleanUp()
.
Quando você terminar de inserir o código final do UserManager.kt, ele ficará assim.
UserManager.kt
@Singleton
class UserManager @Inject constructor(
private val storage: Storage,
// Since UserManager will be in charge of managing the UserComponent lifecycle,
// it needs to know how to create instances of it
private val userDataRepository: UserDataRepository
) {
val username: String
get() = storage.getString(REGISTERED_USER)
fun isUserLoggedIn() = userDataRepository.username != null
fun isUserRegistered() = storage.getString(REGISTERED_USER).isNotEmpty()
fun registerUser(username: String, password: String) {
storage.setString(REGISTERED_USER, username)
storage.setString("$username$PASSWORD_SUFFIX", password)
userJustLoggedIn(username)
}
fun loginUser(username: String, password: String): Boolean {
val registeredUser = this.username
if (registeredUser != username) return false
val registeredPassword = storage.getString("$username$PASSWORD_SUFFIX")
if (registeredPassword != password) return false
userJustLoggedIn(username)
return true
}
fun logout() {
userDataRepository.cleanUp()
}
fun unregister() {
val username = storage.getString(REGISTERED_USER)
storage.setString(REGISTERED_USER, "")
storage.setString("$username$PASSWORD_SUFFIX", "")
logout()
}
private fun userJustLoggedIn(username: String) {
// When the user logs in, we create populate data in UserComponent
userDataRepository.initData(username)
}
}
Agora que você terminou o trabalho com o UserManager
, precisa fazer algumas mudanças no UserDataRepository
. Abra UserDataRepository.kt e faça as mudanças a seguir.
- Remova o
@LoggedUserScope
, porque essa dependência será gerenciada pelo Hilt. - O
UserDataRepository
já está injetado noUserManager
. Então, para evitar uma dependência cíclica, remova o parâmetroUserManager
do construtor doUserDataRepository
. - Mude o
unreadNotifications
para anulável e torne o setter privado. - Adicione uma nova variável anulável,
username
, e torne o setter particular. - Adicione uma nova função
initData()
que definausername
eunreadNotifications
como números aleatórios. - Adicione uma nova função
cleanUp()
para redefinir a contagem deusername
eunreadNotifications
. Defina ousername
como nulo eunreadNotifications
como -1. - Por fim, mova a função
randomInt()
para o corpo da classe.
Quando terminar, o código final ficará assim.
UserDataRepository.kt
@Singleton
class UserDataRepository @Inject constructor() {
var username: String? = null
private set
var unreadNotifications: Int? = null
private set
init {
unreadNotifications = randomInt()
}
fun refreshUnreadNotifications() {
unreadNotifications = randomInt()
}
fun initData(username: String) {
this.username = username
unreadNotifications = randomInt()
}
fun cleanUp() {
username = null
unreadNotifications = -1
}
private fun randomInt(): Int {
return Random.nextInt(until = 100)
}
}
Para concluir a migração do UserComponent
, abra UserComponent.kt e role para baixo até os métodos inject()
. Essa dependência é usada na MainActivity
e na SettingsActivity
. Vamos começar pela migração da MainActivity
. Abra MainActivity.kt e anote a classe com @AndroidEntryPoint
.
MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
//...
}
Remova a interface UserManagerEntryPoint
e o código associado ao ponto de entrada de onCreate()
.
MainActivity.kt
//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface UserManagerEntryPoint {
// fun userManager(): UserManager
//}
override fun onCreate(savedInstanceState: Bundle?) {
//val entryPoint = EntryPoints.get(applicationContext, UserManagerEntryPoint::class.java)
//val userManager = entryPoint.userManager()
super.onCreate(savedInstanceState)
//...
}
Declare uma lateinit var
para o UserManager
e anote-a com @Inject
para que o Hilt possa injetar a dependência.
MainActivity.kt
@Inject
lateinit var userManager: UserManager
Como o UserManager
será injetado pelo Hilt, remova a chamada inject()
no UserComponent
.
MainActivity.kt
//Remove
//userManager.userComponent!!.inject(this)
setupViews()
}
}
Isso é tudo o que você precisa fazer para a MainActivity
. Agora, é possível fazer mudanças semelhantes para migrar a SettingsActivity
. Abra a SettingsActivity
e anote-a com @AndroidEntryPoint
.
SettingsActivity.kt
@AndroidEntryPoint
class SettingsActivity : AppCompatActivity() {
//...
}
Crie uma lateinit var
para o UserManager
e anote-a com @Inject
.
SettingsActivity.kt
@Inject
lateinit var userManager: UserManager
Remova o código do ponto de entrada e a chamada de injeção do userComponent()
. Quando você terminar, a função onCreate()
ficará assim.
SettingsActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
setupViews()
}
Agora, você pode apagar os recursos não utilizados para concluir a migração. Exclua as classes LoggedUserScope.kt, UserComponent.kt e AppSubcomponent.kt.
Agora, execute e teste o app novamente. O app funcionará da mesma forma que fazia com o Dagger.
Falta apenas uma etapa importante para você concluir a migração do app para o Hilt. Até agora, você migrou todo o código do app, mas não os testes. O Hilt injeta dependências em testes, assim como faz no código do app. O teste com o Hilt não requer manutenção, porque ele gera automaticamente um novo conjunto de componentes para cada teste.
Testes de unidade
Vamos começar pelos testes de unidades. Não é necessário usar o Hilt para testes de unidades, porque você pode chamar diretamente o construtor da classe de destino transmitindo dependências falsas ou simuladas, como faria se o construtor não fosse anotado.
Se você executar os testes de unidade, verá que o UserManagerTest está falhando. Você fez muita preparação e mudanças no UserManager, incluindo os parâmetros de construtor nas seções anteriores. Abra o UserManagerTest.kt, que ainda depende do UserComponent
e do UserComponentFactory
. Como você já modificou os parâmetros do UserManager
, mude o parâmetro UserComponent.Factory
com uma nova instância do UserDataRepository
.
UserManagerTest.kt
@Before
fun setup() {
storage = FakeStorage()
userManager = UserManager(storage, UserDataRepository())
}
Isso é tudo. Execute os testes de unidade novamente, e todos eles provavelmente serão aprovados.
Adicionar dependências de teste
Antes de começar, abra app/build.gradle
e confirme se as seguintes dependências do Hilt existem. O Hilt usa hilt-android-testing
para anotações específicas de teste. Além disso, como Hilt precisa gerar código para classes na pasta androidTest
, o processador de anotações dele também precisa ser executado nesse local.
app/build.gradle
// Hilt testing dependencies
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
Testes de IU
O Hilt gera componentes e um Application de teste automaticamente para cada teste. Para começar, abra o TestAppComponent.kt para planejar a migração. O TestAppComponent
tem dois módulos, TestStorageModule
e AppSubcomponents
. Você já migrou e excluiu os AppSubcomponents
. Agora, continue com a migração do TestStorageModule
.
Abra o TestStorageModule.kt e anote a classe com @InstallIn
.
TestStorageModule.kt
@InstallIn(ApplicationComponent::class)
@Module
abstract class TestStorageModule {
//...
Como você terminou de migrar todos os módulos, exclua o TestAppComponent
.
Agora, vamos adicionar o Hilt ao ApplicationTest
. Anote qualquer teste de IU que use o Hilt com @HiltAndroidTest
. Essa anotação é responsável por gerar os componentes do Hilt para cada teste.
Abra ApplicationTest.kt e adicione as seguintes anotações:
@HiltAndroidTest
para instruir o Hilt a gerar componentes para esse teste.@UninstallModules(StorageModule::class)
para instruir o Hilt a desinstalar oStorageModule
declarado no código do app. Assim, durante os testes, oTestStorageModule
será injetado.- Você também precisa adicionar uma
HiltAndroidRule
aoApplicationTest
. Essa regra de teste gerencia o estado dos componentes e é usada para fazer a injeção no teste. O código final ficará assim.
ApplicationTest.kt
@UninstallModules(StorageModule::class)
@HiltAndroidTest
class ApplicationTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
//...
Como o Hilt gera um novo Application
para cada teste de instrumentação, é necessário especificar que o Application
gerado pelo Hilt precisa ser usado ao executar testes de IU. Para isso, precisamos de um executor de testes personalizado.
O app do codelab já tem um. Abra MyCustomTestRunner.kt
.
O Hilt já vem com um Application
chamado HiltTestApplication.
que você pode usar para testes. É necessário modificar MyTestApplication::class.java
com HiltTestApplication::class.java
no corpo da função newApplication()
.
MyCustomTestRunner.kt
class MyCustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
Com essa mudança, agora é seguro excluir o arquivo MyTestApplication.kt. Execute os testes. Provavelmente, todos eles serão aprovados.
O Hilt inclui extensões para fornecer classes de outras bibliotecas do Jetpack, como WorkManager e ViewModel. Os ViewModels do projeto do codelab são classes simples que não estendem ViewModel
dos componentes da arquitetura. Antes de adicionar compatibilidade do Hilt com ViewModels, vamos migrar os ViewModels do app para os componentes da arquitetura.
Para fazer a integração com o ViewModel
, é necessário adicionar as seguintes dependências ao seu arquivo do Gradle. Essas dependências já foram adicionadas para você. Além da biblioteca, você precisa adicionar um processador de anotações extra que funcione com o processador de anotações do Hilt:
// app/build.gradle file
...
dependencies {
...
implementation "androidx.fragment:fragment-ktx:1.2.4"
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:$hilt_jetpack_version'
kapt 'androidx.hilt:hilt-compiler:$hilt_jetpack_version'
kaptAndroidTest 'androidx.hilt:hilt-compiler:$hilt_jetpack_version'
}
Para migrar uma classe simples para o ViewModel
, é necessário estender o ViewModel()
.
Abra MainViewModel.kt e adicione : ViewModel()
. Isso é suficiente para migrar para os ViewModels dos componentes da arquitetura, mas também é preciso informar ao Hilt como fornecer instâncias do ViewModel. Para fazer isso, adicione a anotação @ViewModelInject
no construtor do ViewModel
. Substitua a anotação @Inject
por @ViewModelInject
.
MainViewModel.kt
class MainViewModel @ViewModelInject constructor(
private val userDataRepository: UserDataRepository
): ViewModel() {
//...
}
Em seguida, abra o LoginViewModel
e faça as mesmas modificações. O código final ficará assim.
LoginViewModel.kt
class LoginViewModel @ViewModelInject constructor(
private val userManager: UserManager
): ViewModel() {
//...
}
Da mesma forma, abra o RegistrationViewModel.kt, migre para o ViewModel()
e adicione a anotação Hilt. Você não precisa da anotação @ActivityScoped
, já que com os métodos de extensão viewModels()
e activityViewModels()
é possível controlar o escopo desse ViewModel
.
RegistrationViewModel.kt
class RegistrationViewModel @ViewModelInject constructor(
val userManager: UserManager
) : ViewModel() {
Faça as mesmas mudanças para migrar o EnterDetailsViewModel
e o SettingViewModel
. O código final dessas duas classes ficará assim.
EnterDetailsViewModel.kt
class EnterDetailsViewModel @ViewModelInject constructor() : ViewModel() {
SettingViewModel.kt
class SettingsViewModel @ViewModelInject constructor(
private val userDataRepository: UserDataRepository,
private val userManager: UserManager
) : ViewModel() {
Agora que todos os ViewModels foram migrados para os Viewmodels dos componentes da arquitetura e receberam anotações do Hilt, você pode migrar a forma como eles são injetados.
Em seguida, mude a forma como os ViewModels são inicializados na camada View. Os ViewModels são criados pelo SO, e você pode consegui-los usando a função delegada by viewModels()
.
Abra MainActivity.kt, substitua a anotação @Inject
pelas extensões do Jetpack. Você também precisa remover a lateinit
, mudar var
para val
e marcar o campo private
.
MainActivity.kt
// @Inject
// lateinit var mainViewModel: MainViewModel
private val mainViewModel: MainViewModel by viewModels()
Da mesma forma, abra LoginActivity.kt e mude a forma como o ViewModel
é obtido.
LoginActivity.kt
// @Inject
// lateinit var loginViewModel: LoginViewModel
private val loginViewModel: LoginViewModel by viewModels()
Em seguida, abra RegistrationActivity.kt e aplique modificações semelhantes para conseguir o registrationViewModel
.
RegistrationActivity.kt
// @Inject
// lateinit var registrationViewModel: RegistrationViewModel
private val registrationViewModel: RegistrationViewModel by viewModels()
Abra EnterDetailsFragment.kt. Mude a forma como o EnterDetailsViewModel
é obtido.
EnterDetailsFragment.kt
private val enterDetailsViewModel: EnterDetailsViewModel by viewModels()
Da mesma forma, mude a forma como o registrationViewModel
é obtido. Mas, desta vez, use a função de delegação activityViewModels()
em vez de viewModels().
. Quando o registrationViewModel
é injetado, o Hilt injeta o ViewModel com escopo no nível da atividade.
EnterDetailsFragment.kt
private val registrationViewModel: RegistrationViewModel by activityViewModels()
Abra TermsAndConditionsFragment.kt e use novamente a função de extensão activityViewModels()
em vez de viewModels()
para conseguir registrationViewModel.
.
TermsAndConditionsFragment.kt
private val registrationViewModel: RegistrationViewModel by activityViewModels()
Por fim, abra SettingsActivity.kt e migre o modo como o settingsViewModel
é recebido.
SettingsActivity.kt
private val settingsViewModel: SettingsViewModel by viewModels()
Agora, execute o app e confirme se tudo está funcionando como esperado.
Parabéns! Você migrou um app para o Hilt. Além de concluir a migração, você também manteve o app funcionando ao migrar componentes do Dagger.
Neste codelab, você aprendeu a usar o componente Application e a criar a base necessária para fazer o Hilt funcionar com os componentes do Dagger. Depois, você migrou cada componente do Dagger para o Hilt usando anotações do Hilt em atividades e fragmentos e removendo o código associado ao Dagger. Cada vez que terminou de migrar um componente, o app funcionou conforme o esperado. Você também migrou as dependências Context
e ApplicationContext
com as anotações @ActivityContext
e @ApplicationContext
fornecidas pelo Hilt. Você migrou outros componentes do Android. Por fim, você migrou os testes e concluiu a migração para o Hilt.
Leia mais
Para saber mais sobre a migração do seu app para o Hilt, confira a documentação Migrar para o Hilt (link em inglês). Além de mais informações sobre a migração do Dagger para o Hilt, você também pode aprender a migrar um app dagger.android.