Como migrar seu app Dagger para o Hilt

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:

Fazer o download do 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:

Fazer o download do código final

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 execute.png e escolha um emulador ou conecte seu dispositivo Android. A tela de registro será exibida.

54d4e2a9bf8177c1.gif

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.

8ecf1f9088eb2bb6.png

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:

a1b8656d7fc17b7d.png

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 na LoginActivity
  • O RegistrationComponent é injetado na RegistrationActivity, no EnterDetailsFragment e no TermsAndConditionsFragment. Esse componente também está no escopo da RegistrationActivity.

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:

  1. Deixar o Hilt e o Dagger lado a lado no estado em que o projeto está.
  2. 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 pelo UserDataRepository no construtor, porque não é mais necessário criar uma instância do UserComponent. Ele tem o UserDataRepository 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 do userRepository, em vez do userComponent.
  • Adicione o nome de usuário como um parâmetro à função userJustLoggedIn().
  • Mude o corpo da função userJustLoggedIn() para chamar initData com userName no userDataRepository, em vez de userComponent, que você excluirá durante a migração.
  • Adicione o username à chamada userJustLoggedIn() nas funções registerUser() e loginUser().
  • Remova o userComponent da função logout() e substitua-o por uma chamada para userDataRepository.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 no UserManager. Então, para evitar uma dependência cíclica, remova o parâmetro UserManager do construtor do UserDataRepository.
  • 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 defina username e unreadNotifications como números aleatórios.
  • Adicione uma nova função cleanUp() para redefinir a contagem de username e unreadNotifications. Defina o username como nulo e unreadNotifications 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 o StorageModule declarado no código do app. Assim, durante os testes, o TestStorageModule será injetado.
  • Você também precisa adicionar uma HiltAndroidRule ao ApplicationTest. 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.