Cómo migrar tu app de Dagger a Hilt

En este codelab, aprenderás a migrar Dagger a Hilt para realizar una inserción de dependencias (DI) en una app para Android. Este codelab realiza la migración a Hilt usando Dagger en tu app para Android. El objetivo de este codelab es mostrarte cómo planificar la migración para que Dagger y Hilt trabajen de manera simultánea durante el proceso y, así, mantener la funcionalidad de la app mientras los componentes de Dagger se migran a Hilt.

La inserción de dependencias ayuda con la reutilización de código y facilita la refactorización y las pruebas. Hilt se basa en la popular biblioteca de inserción de dependencias Dagger y se beneficia de la corrección en tiempo de compilación, el rendimiento del entorno de ejecución, la escalabilidad y la compatibilidad con Android Studio que proporciona esta herramienta.

Por otro lado, como el SO crea instancias de las clases del framework de Android, hay código estándar asociado cuando se usa Dagger en apps para Android. Hilt quita la mayor parte de este código estándar generando y proporcionando automáticamente lo siguiente:

  • Componentes para integrar clases de frameworks de Android con Dagger que, de lo contrario, deberías crear a mano
  • Anotaciones de alcance de los componentes que Hilt genera automáticamente
  • Vinculación y calificadores predefinidos

Lo mejor de todo es que, como Dagger y Hilt pueden coexistir, las apps se pueden migrar cada vez que sea necesario.

Si, a medida que avanzas con este codelab, encuentras algún problema (errores de código, errores gramaticales, texto poco claro, etc.), infórmalo mediante el vínculo Informa un error que se encuentra en la esquina inferior izquierda del codelab.

Requisitos previos

  • Experiencia con la sintaxis de Kotlin
  • Experiencia con Dagger

Qué aprenderás

  • Cómo agregar Hilt a tu app para Android
  • Cómo planificar tu estrategia de migración
  • Cómo migrar componentes a Hilt y mantener funcionando el código de Dagger existente
  • Cómo migrar componentes con alcance
  • Cómo probar tu app con Hilt

Requisitos

  • Android Studio 4.0 o una versión posterior

Obtén el código

Obtén el código del codelab de GitHub:

$ git clone https://github.com/googlecodelabs/android-dagger-to-hilt

De manera alternativa, puedes descargar el repositorio en un archivo ZIP:

Descargar ZIP

Abre Android Studio

Si necesitas descargar Android Studio, puedes hacerlo aquí.

Configura el proyecto

El proyecto se compila en varias ramas de GitHub:

  • master es la rama que revisaste o descargaste. Es el punto de partida del codelab.
  • interop es la rama de interoperación entre Dagger y Hilt.
  • solution contiene la solución para este codelab, incluidas las pruebas y los ViewModels.

Te recomendamos que sigas el codelab paso a paso a tu propio ritmo comenzando con la rama master.

Durante el proceso, te mostraremos fragmentos de código que deberás agregar al proyecto. En algunos lugares, también deberás quitar el código que se mencionará explícitamente en los comentarios de los fragmentos de código.

Como puntos de control, tendrás las ramas intermedias disponibles en caso de que necesites ayuda con un paso en particular.

Para obtener la rama solution con Git, usa el siguiente comando:

$ git clone -b solution https://github.com/googlecodelabs/android-dagger-to-hilt

También puedes descargar el código de la solución aquí:

Descargar el código final

Preguntas frecuentes

Ejecuta la app de muestra

Primero, veamos el aspecto de la app de ejemplo inicial. Sigue estas instrucciones para abrir la app de muestra en Android Studio.

  • Si descargaste el archivo ZIP, descomprímelo en una ubicación local.
  • Abre el proyecto en Android Studio.
  • Haz clic en el botón Run execute.png y elige un emulador o conecta tu dispositivo Android. Debería aparecer la pantalla de registro.

54d4e2a9bf8177c1.gif

La app consta de 4 flujos diferentes que trabajan con Dagger (implementados como elementos Activity):

  • Registro: El usuario puede registrarse si ingresa el nombre de usuario y la contraseña, y acepta los Términos y Condiciones.
  • Acceso: El usuario puede acceder con las credenciales que agregó durante el flujo de registro y también puede anular el registro en la app.
  • Página principal: Le da la bienvenida al usuario y le muestra cuántas notificaciones no leídas tiene.
  • Configuración: El usuario puede salir de la cuenta y actualizar la cantidad de notificaciones no leídas (lo que genera una cantidad aleatoria de notificaciones).

El proyecto sigue un patrón de MVVM clásico, en el que toda la complejidad de la View se difiere a un ViewModel. Tómate un momento para familiarizarte con la estructura del proyecto.

8ecf1f9088eb2bb6.png

Las flechas representan dependencias entre objetos. Esto es lo que llamamos gráfico de la aplicación: Todas las clases de la app y las dependencias entre ellas.

El código de la rama master usa Dagger para insertar dependencias. En lugar de crear componentes manualmente, refactorizamos la app para que use Hilt a fin de generar componentes y otro código relacionado con Dagger.

Dagger se configura en la app como se muestra en el siguiente diagrama. El punto en ciertos tipos indica que su alcance llega al componente que los proporciona:

a1b8656d7fc17b7d.png

Para simplificar el proceso, las dependencias de Hilt ya se agregan a este proyecto en la rama master que descargues al principio. No es necesario que agregues el siguiente código a tu proyecto, puesto que ya viene listo. No obstante, veamos qué se necesita para usar Hilt en una app para Android.

Además de las dependencias de la biblioteca, Hilt usa un complemento de Gradle configurado en el proyecto. Abre el archivo raíz build.gradle (nivel del proyecto) y busca la siguiente dependencia de Hilt en la ruta de clase:

buildscript {
    ...
    ext.hilt_version = '2.28-alpha'
    dependencies {
        ...
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}

Abre app/build.gradle y revisa la declaración del complemento de Gradle para Hilt en la parte superior, debajo del complemento kotlin-kapt.

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

Por último, el proyecto incluye las dependencias de Hilt y el procesador de anotaciones en el mismo archivo app/build.gradle:

...
dependencies {
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}

Todas las bibliotecas, incluso Hilt, se descargan cuando compilas y sincronizas el proyecto. ¡Comienza a usar Hilt!

En posible que tengas la tentación de migrar todo el contenido a Hilt de una vez, pero que en un proyecto real quieras que la app se ejecute sin errores mientras haces la migración a Hilt por pasos.

Cuando vayas a migrar a Hilt, deberás organizar tu trabajo en pasos. Te recomendamos que comiences con la migración de tu componente @Singleton o Application y, luego, migres las actividades y los fragmentos.

En el codelab, primero migrarás AppComponent y, luego, cada flujo de la app en este orden: registro, acceso y, por último, actividad principal y configuración.

Durante la migración, quitarás todas las interfaces @Component y @Subcomponent, y anotarás todos los módulos con @InstallIn.

Después de la migración, todas las clases Application/Activity/Fragment/View/Service/BroadcastReceiver deben anotarse con @AndroidEntryPoint y el código que crea instancias de componentes o los propaga debe quitarse.

Para planificar la migración, comencemos con AppComponent.kt a fin de comprender la jerarquía 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
}

AppComponent tiene anotaciones con @Component y, además, incluye dos módulos: StorageModule y AppSubcomponents.

AppSubcomponents tiene tres componentes: RegistrationComponent, LoginComponent y UserComponent.

  • LoginComponent se inserta en LoginActivity.
  • RegistrationComponent se inserta en RegistrationActivity, EnterDetailsFragment y TermsAndConditionsFragment. Este componente también tiene alcance en RegistrationActivity.

UserComponent se inserta en MainActivity y SettingsActivity.

Las referencias a ApplicationComponent se pueden reemplazar por el componente generado por Hilt (vínculo a todos los componentes generados) que se mapea al componente que migrarás en tu app.

En esta sección, migrarás AppComponent. Deberás establecer la base para que el código de Dagger existente siga funcionando, ya que en los próximos pasos migrarás cada componente a Hilt.

Para inicializar Hilt y comenzar con la generación de código, debes anotar tu clase Application con anotaciones de Hilt.

Abre MyApplication.kt y agrega la anotación @HiltAndroidApp a la clase. Estas anotaciones le indican a Hilt que active la generación de código que Dagger recopilará y usará en su procesador de anotaciones.

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. Migra módulos de componentes

Para comenzar, abre AppComponent.kt. AppComponent tiene dos módulos (StorageModule y AppSubcomponents) agregados en la anotación @Component. Lo primero que debes hacer es migrar estos 2 módulos para que Hilt los agregue al ApplicationComponent generado.

Para hacerlo, abre AppSubcomponents.kt y anota la clase con la anotación @InstallIn. La anotación @InstallIn toma un parámetro para agregar el módulo al componente correcto. En este caso, mientras migras el componente de nivel de la aplicación, quieres que las vinculaciones se generen en 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

Debes hacer el mismo cambio en StorageModule. Abre StorageModule.kt y agrega la anotación @InstallIn como lo hiciste en el paso 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
}

Con la anotación @InstallIn, una vez más, le indicas a Hilt que agregue el módulo al ApplicationComponent generado por Hilt.

Ahora, volvamos a verificar AppComponent.kt. AppComponent proporciona dependencias para RegistrationComponent, LoginComponent y UserManager. En los siguientes pasos, prepararás estos componentes para la migración.

2. Migra los tipos expuestos

Mientras migras la app completa a Hilt, este te permite solicitar manualmente dependencias de Dagger mediante puntos de entrada. Si usas puntos de entrada, puedes hacer que la app siga funcionando mientras se migran todos los componentes de Dagger. En este paso, reemplazarás cada componente de Dagger con una búsqueda de dependencia manual en el ApplicationComponent generado por Hilt.

A fin de obtener el RegistrationComponent.Factory para la RegistrationActivity.kt del ApplicationComponent generado por Hilt, debes crear una nueva interfaz EntryPoint con anotación @InstallIn. La anotación InstallIn le indica a Hilt dónde debe obtener la vinculación. Para acceder a un punto de entrada, usa el método estático apropiado de EntryPointAccessors. El parámetro debería ser la instancia del componente o el objeto @AndroidEntryPoint que funciona como contenedor del componente.

RegistrationActivity.kt

class RegistrationActivity : AppCompatActivity() {

    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface RegistrationEntryPoint {
        fun registrationComponent(): RegistrationComponent.Factory
    }

    ...
}

Ahora, debes reemplazar el código relacionado con Dagger por RegistrationEntryPoint. Cambia la inicialización de registrationComponent para usar RegistrationEntryPoint. Con este cambio, RegistrationActivity puede acceder a sus dependencias mediante código generado Hilt hasta que se migre para usar Hilt.

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 continuación, debes realizar la misma tarea de base para todos los demás tipos de componentes expuestos. Continuemos con LoginComponent.Factory. Abre LoginActivity y crea una interfaz LoginEntryPoint con anotaciones @InstallIn y @EntryPoint, como lo hiciste antes, pero muestra lo que LoginActivity necesita del componente Hilt.

LoginActivity.kt

    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface LoginEntryPoint {
        fun loginComponent(): LoginComponent.Factory
    }

Ahora que Hilt sabe cómo proporcionar el LoginComponent, reemplaza la llamada anterior de inject() por el loginComponent() de EntryPoint.

LoginActivity.kt

        val entryPoint = EntryPointAccessors.fromApplication(applicationContext, LoginEntryPoint::class.java)
        entryPoint.loginComponent().create().inject(this)

Dos de los tres tipos expuestos de AppComponent se reemplazan para hacerlos funcionar con EntryPoint de Hilt. A continuación, debes realizar un cambio similar para UserManager. A diferencia de RegistrationComponent y LoginComponent, UserManager se usa tanto en MainActivity como en SettingsActivity. Solo debes crear una interfaz EntryPoint una vez. La interfaz EntryPoint con anotaciones puede usarse en ambas Activity. Para que sea más simple, declara la interfaz en MainActivity.

Para crear una interfaz UserManagerEntryPoint, abre MainActivity.kt y anótala con @InstallIn y @EntryPoint.

MainActivity.kt

    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface UserManagerEntryPoint {
        fun userManager(): UserManager
    }

Ahora, cambia UserManager para usar UserManagerEntryPoint.

MainActivity.kt

        val entryPoint = EntryPointAccessors.fromApplication(applicationContext, UserManagerEntryPoint::class.java)
        val userManager = entryPoint.userManager()

Debes hacer el mismo cambio en SettingsActivity. Abre SettingsActivity.kt y reemplaza la forma en que se inserta UserManager.

SettingsActivity.kt

    val entryPoint = EntryPointAccessors.fromApplication(applicationContext, MainActivity.UserManagerEntryPoint::class.java)
    val userManager = entryPoint.userManager()

3. Quita la fábrica de componentes

Pasar un Context a un componente de Dagger con @BindsInstance es un patrón común. Esto no se necesita en Hilt, puesto que Context ya está disponible como vinculación predefinida.

En general, Context se necesita para acceder a recursos, bases de datos, preferencias compartidas, etc. Hilt simplifica la inserción en el contexto mediante el calificador @ApplicationContext y @ActivityContext.

Durante la migración de tu app, comprueba qué tipos requieren Context como dependencia y reemplázalos con los que ofrece Hilt.

En este caso, SharedPreferencesStorage tiene Context como dependencia. Para indicarle a Hilt que inserte el contexto, abre SharedPreferencesStorage.kt. SharedPreferences, que requiere el Context de la aplicación, debes agregar la anotación @ApplicationContext al parámetro contextual.

SharedPreferencesStorage.kt

class SharedPreferencesStorage @Inject constructor(
    @ApplicationContext context: Context
) : Storage {

//...

4. Migra los métodos de inserción

A continuación, debes verificar el código de componente para los métodos inject() y anotar las clases correspondientes con @AndroidEntryPoint. En nuestro caso, como AppComponent no tiene ningún método inject(), no necesitas hacer nada.

5. Quita la clase AppComponent

Como ya agregaste EntryPoints para todos los componentes enumerados en AppComponent.kt, puedes borrar AppComponent.kt.

6. Quita el código que usa el componente para migrar

Ya no necesitas el código para inicializar el AppComponent personalizado en la clase de la aplicación. En su lugar, la clase Application usa el ApplicationComponent generado por Hilt. Quita todo el código dentro del cuerpo de la clase. El código final debería verse como la siguiente lista de códigos.

MyApplication.kt

package com.example.android.dagger

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
open class MyApplication : Application()

Con esto, habrás agregado correctamente Hilt a tu aplicación, quitado AppComponent y cambiado el código de Dagger para insertar dependencias en el AppComponent generado por Hilt. Cuando compiles y pruebes la app en un dispositivo o emulador, esta debería funcionar como antes. En las siguientes secciones, migraremos cada Activity y Fragment para usar Hilt.

Ahora que migraste el componente de la aplicación y estableciste las bases, puedes migrar uno por uno cada Component a Hilt.

Comencemos a migrar el flujo de acceso. En lugar de crear el LoginComponent manualmente y usarlo en el LoginActivity, queremos que Hilt lo haga automáticamente.

Puedes seguir los mismos pasos de la sección anterior, pero esta vez usando el ActivityComponent generado por Hilt, ya que vamos a migrar un componente administrado por una Activity.

Para comenzar, abre LoginComponent.kt. LoginComponent no tiene ningún módulo, por lo que no debes hacer nada. Para que Hilt genere un componente para LoginActivity y lo inserte, debes anotar la actividad con @AndroidEntryPoint.

LoginActivity.kt

@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {

    //...
}

Este es el código completo que debes agregar para migrar LoginActivity a Hilt. Como Hilt generará el código relacionado con Dagger, lo único que debes hacer es una limpieza. Borra la interfaz LoginEntryPoint.

LoginActivity.kt

    //Remove
    //@InstallIn(ApplicationComponent::class)
    //@EntryPoint
    //interface LoginEntryPoint {
    //    fun loginComponent(): LoginComponent.Factory
    //}

Luego, quita el código de EntryPoint de 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 Hilt generará el componente, busca y borra LoginComponent.kt.

Actualmente, LoginComponent aparece como subcomponente en AppSubcomponents.kt. Puedes borrar LoginComponent de forma segura de la lista de subcomponentes, ya que Hilt generará las vinculaciones por ti.

AppSubcomponents.kt

// This module tells a Component which are its subcomponents
@InstallIn(ApplicationComponent::class)
@Module(
    subcomponents = [
        RegistrationComponent::class,
        UserComponent::class
    ]
)
class AppSubcomponents

Esto es todo lo que necesitas para migrar LoginActivity y usar Hilt. En esta sección, borraste mucho más código que el que agregaste, lo cual es genial. A su vez, como escribes menos código cuando usas Hilt, tendrás menos código que mantener y, por ende, menor probabilidad de cometer errores.

En esta sección, migrarás el flujo de registro. Para planificar la migración, veamos RegistrationComponent. Abre RegistrationComponent.kt y desplázate hacia abajo hasta la función inject(). RegistrationComponent es responsable de insertar dependencias en RegistrationActivity, EnterDetailsFragment y TermsAndConditionsFragment.

Comencemos con la migración de RegistrationActivity. Abre RegistrationActivity.kt y anota la clase con @AndroidEntryPoint.

RegistrationActivity.kt

@AndroidEntryPoint
class RegistrationActivity : AppCompatActivity() {
    //...
}

Ahora que RegistrationActivity se registró en Hilt, puedes quitar la interfaz RegistrationEntryPoint y el código relacionado de EntryPoint de la función 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)
    //..
}

Hilt es responsable de generar el componente y de insertar las dependencias a fin de que puedas quitar la variable registrationComponent y la llamada de inserción en el componente de Dagger borrado.

RegistrationActivity.kt

// Remove
// lateinit var registrationComponent: RegistrationComponent

override fun onCreate(savedInstanceState: Bundle?) {
    //Remove
    //registrationComponent.inject(this)
    super.onCreate(savedInstanceState)

    //..
}

A continuación, abre EnterDetailsFragment.kt. Anota el EnterDetailsFragment con @AndroidEntryPoint, de manera similar a como lo hiciste en RegistrationActivity.

EnterDetailsFragment.kt

@AndroidEntryPoint
class EnterDetailsFragment : Fragment() {

    //...
}

Como Hilt proporciona las dependencias, no se necesita la llamada inject() en el componente de Dagger borrado. Borra la función onAttach().

El siguiente paso es migrar TermsAndConditionsFragment. Abre TermsAndConditionsFragment.kt, anota la clase y quita la función onAttach() como lo hiciste en el paso anterior. El código final debería verse así:

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
    }
}

Con este cambio, habrás migrado todas las actividades y los fragmentos que se enumeran en RegistrationComponent, por lo que puedes borrar RegistrationComponent.kt.

Después de borrar RegistrationComponent, debes quitar su referencia de la lista de subcomponentes de AppSubcomponents.

AppSubcomponents.kt

@InstallIn(ApplicationComponent::class)
// This module tells a Component which are its subcomponents
@Module(
    subcomponents = [
        UserComponent::class
    ]
)
class AppSubcomponents

Solo resta un paso para completar la migración del flujo de registro. El flujo de registro declara y usa su propio alcance, ActivityScope. Los alcances controlan el ciclo de vida de las dependencias. En este caso, ActivityScope le indica a Dagger que inserte la misma instancia de RegistrationViewModel dentro del flujo iniciado con RegistrationActivity. Hilt proporciona alcances de ciclo de vida integrados para admitir esta capacidad.

Abre @ActivityScope y cambia la anotación RegistrationViewModel con la @ActivityScoped proporcionada por Hilt.

RegistrationViewModel.kt

@ActivityScoped
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {

    //...
}

Como ActivityScope no se usa en ningún otro lugar, puedes borrar ActivityScope.kt.

Ahora, ejecuta la app y prueba el flujo de registro. Puedes usar tu nombre de usuario y contraseña actuales para acceder o anular el registro y volver a registrarte con una cuenta nueva para confirmar que el flujo funciona del mismo modo que antes.

En ese momento, Dagger y Hilt estarán trabajando de manera simultánea en la app. Hilt insertará todas las dependencias, excepto UserManager. En la siguiente sección, migrarás por completo a Hilt desde Dagger realizando la migración de UserManager.

Hasta ahora, en este codelab, migraste correctamente la mayor parte de la app de muestra a Hilt, excepto por un componente, UserComponent. UserComponent tiene anotaciones con un alcance personalizado, @LoggedUserScope. Esto significa que UserComponent insertará la misma instancia de UserManager en las clases anotadas con @LoggedUserScope.

UserComponent no se mapea a ninguno de los componentes de Hilt disponibles, puesto que su ciclo de vida no es administrado por una clase de Android. Como no se admite la adición de un componente personalizado en el medio de la jerarquía generada por Hilt, tienes dos opciones:

  1. Permitir que Hilt y Dagger trabajen de manera simultánea en el estado en que se encuentre el proyecto en ese momento.
  2. Migrar el componente en el alcance al componente Hilt disponible más cercano (ApplicationComponent en este caso) y usar la nulidad cuando sea necesario.

Ya completaste el primer paso. El siguiente será migrar por completo la aplicación a Hilt. Sin embargo, para una app real, puedes elegir el flujo que mejor se adapte a tu caso de uso específico.

En este paso, se migrará UserComponent para que forme parte del ApplicationComponent de Hilt. Si el componente tiene módulos, estos también deben instalarse en ApplicationComponent.

El único tipo de alcance en UserComponent es UserDataRepository, que se anota con @LoggedUserScope. Como UserComponent convergerá con el ApplicationComponent de Hilt, UserDataRepository se anotará con @Singleton, lo que te permitirá cambiar la lógica para que sea nulo cuando el usuario salga de la cuenta.

UserManager ya tiene la anotación @Singleton, lo que significa que puedes proporcionar la misma instancia en toda la app y, con algunos cambios, podrás conseguir la misma funcionalidad con Hilt. Comencemos por cambiar el funcionamiento de UserManager y UserDataRepository, ya que primero debes establecer las bases.

Abre UserManager.kt y aplica los siguientes cambios.

  • Reemplaza el parámetro UserComponent.Factory por UserDataRepository en el constructor, puesto que ya no necesitas crear una instancia de UserComponent. En su lugar, tiene UserDataRepository como dependencia.
  • Como Hilt generará el código del componente, borrará UserComponent y su método set.
  • Cambia la función isUserLoggedIn() para verificar el nombre de usuario de userRepository, en lugar de verificar userComponent.
  • Agrega el nombre de usuario como parámetro a la función userJustLoggedIn().
  • Cambia el cuerpo de la función userJustLoggedIn() para llamar a initData con userName en userDataRepository, en lugar de userComponent, que borrarás durante la migración.
  • Agrega la llamada username a userJustLoggedIn() en las funciones registerUser() y loginUser().
  • Quita userComponent de la función logout() y reemplázalo con una llamada a userDataRepository.cleanUp().

Cuando termines, el código final de UserManager.kt debería verse así:

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)
    }
}

Ahora que terminaste con UserManager, debes hacer algunos cambios en UserDataRepository. Abre UserDataRepository.kt y aplica los siguientes cambios.

  • Quita @LoggedUserScope, ya que Hilt administrará esta dependencia.
  • UserDataRepository ya se insertó en UserManager para evitar una dependencia cíclica. Quita el parámetro UserManager del constructor de UserDataRepository.
  • Cambia unreadNotifications a un valor anulable y haz que el método set sea privado.
  • Agrega una nueva variable anulable, username, y haz que el meto set sea privado.
  • Agrega una nueva función initData(), que establece el username y el unreadNotifications en un número al azar.
  • Agrega una nueva función cleanUp() para restablecer el recuento de username y unreadNotifications. Establece username en un valor nulo y unreadNotifications en -1.
  • Por último, mueve la función randomInt() dentro del cuerpo de la clase.

Cuando termines, el código de finalización debería verse así.

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 finalizar el proceso de migración de UserComponent, abre UserComponent.kt y desplázate hacia abajo hasta los métodos inject(). Esta dependencia se usa en MainActivity y SettingsActivity. Comencemos con la migración de MainActivity. Abre MainActivity.kt y anota la clase con @AndroidEntryPoint.

MainActivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    //...
}

Quita la interfaz de UserManagerEntryPoint y también quita el código relacionado con el punto 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)

    //...
}

Declara un lateinit var para UserManager y anótalo con la anotación @Inject para que Hilt pueda insertar la dependencia.

MainActivity.kt

@Inject
lateinit var userManager: UserManager

Como Hilt insertará UserManager, quita la llamada inject() a UserComponent.

MainActivity.kt

        //Remove
        //userManager.userComponent!!.inject(this)
        setupViews()
    }
}

Es lo único que debes hacer para MainActivity. Ahora, puedes realizar cambios similares para migrar SettingsActivity. Abre SettingsActivity y anótala con @AndroidEntryPoint.

SettingsActivity.kt

@AndroidEntryPoint
class SettingsActivity : AppCompatActivity() {
    //...
}

Crea un lateinit var para UserManager y anótalo con @Inject.

SettingsActivity.kt

    @Inject
    lateinit var userManager: UserManager

Quita el código del punto de entrada y la llamada de inserción en userComponent(). Cuando termines, la función onCreate() debería verse así:

SettingsActivity.kt

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        setupViews()
    }

A continuación, puedes limpiar los recursos sin usar para finalizar la migración. Borra las clases LoggedUserScope.kt y UserComponent.kt y, por último, las clases AppSubcomponent.kt.

Ahora, vuelve a ejecutar y prueba la app. Debería funcionar tal como lo hacía con Dagger.

Sin embargo, antes de finalizar la migración a Hilt, hay un paso clave que debes completar. Hasta ahora, migraste todo el código de la app, pero no las pruebas. Hilt inserta dependencias en pruebas tal como lo hace en el código de la app. Las pruebas con Hilt no requieren mantenimiento, ya que se genera automáticamente un nuevo conjunto de componentes para cada prueba.

Pruebas de unidades

Comencemos con las pruebas de unidades. No necesitas usar Hilt para las pruebas de unidades, ya que puedes llamar directamente al constructor de la clase de destino y pasar dependencias ficticias o simuladas como lo harías si el constructor no estuviera anotado.

Si ejecutas las pruebas de unidades, verás fallas de UserManagerTest. En las secciones anteriores, hiciste un gran trabajo e implementaste cambios en UserManager, incluidos los parámetros de su constructor. Abre UserManagerTest.kt, que aún depende de UserComponent y UserComponentFactory. Como ya cambiaste los parámetros de UserManager, cambia el parámetro UserComponent.Factory con una instancia nueva de UserDataRepository.

UserManagerTest.kt

    @Before
    fun setup() {
        storage = FakeStorage()
        userManager = UserManager(storage, UserDataRepository())
    }

¡Muy bien! Vuelve a ejecutar las pruebas de unidades; ahora todas deberían aprobarse.

Agrega dependencias de prueba

Antes de comenzar, abre app/build.gradle y confirma que existan las siguientes dependencias de Hilt. Hilt usa hilt-android-testing para anotaciones específicas de pruebas. Además, como Hilt necesita generar código para las clases de la carpeta androidTest, su procesador de anotaciones también debe poder ejecutarse allí.

app/build.gradle

    // Hilt testing dependencies
    androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
    kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"

Pruebas de la IU

Hilt genera componentes de prueba y una aplicación de prueba automáticamente para cada prueba. Para comenzar, abre TestAppComponent.kt a fin de planificar la migración. TestAppComponent tiene 2 módulos: TestStorageModule y AppSubcomponents. Como ya migraste y borraste AppSubcomponents, puedes continuar con la migración de TestStorageModule.

Abre TestStorageModule.kt y anota la clase con la anotación @InstallIn.

TestStorageModule.kt

@InstallIn(ApplicationComponent::class)
@Module
abstract class TestStorageModule {
    //...

Como terminaste de migrar todos los módulos, borra TestAppComponent.

A continuación, agregaremos Hilt a ApplicationTest. Debes anotar cualquier prueba de IU que use Hilt con @HiltAndroidTest. Esta anotación es responsable de generar los componentes de Hilt para cada prueba.

Abre ApplicationTest.kt y agrega las siguientes anotaciones:

  • @HiltAndroidTest a fin de indicarle a Hilt que genere componentes para esta prueba
  • @UninstallModules(StorageModule::class) para indicar a Hilt que desinstale el StorageModule declarado en el código de la app de modo que, durante la prueba, se inserten TestStorageModule
  • También debes agregar un HiltAndroidRule a ApplicationTest (esta regla de prueba administra el estado de los componentes y se usa para realizar inserciones en la prueba) El código final debería verse así:

ApplicationTest.kt

@UninstallModules(StorageModule::class)
@HiltAndroidTest
class ApplicationTest {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    //...

Como Hilt genera un elemento Application nuevo para cada prueba de instrumentación, debemos especificar que debería usarse ese elemento Application durante la ejecución de pruebas de IU. Para ello, necesitamos un ejecutor de pruebas personalizado.

La app de codelab ya tiene un ejecutor de prueba personalizado. Abre MyCustomTestRunner.kt

Hilt ya incluye un elemento Application que puedes usar para pruebas llamadas HiltTestApplication.. Debes cambiar MyTestApplication::class.java por HiltTestApplication::class.java en el cuerpo de la función 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)
    }
}

Con este cambio, es seguro borrar el archivo MyTestApplication.kt. Ahora ejecuta las pruebas. Todas deberían aprobarse.

Hilt incluye extensiones para proporcionar clases de otras bibliotecas de Jetpack, como WorkManager y ViewModel. Los ViewModels del proyecto del codelab son clases simples que no extienden ViewModel desde componentes de arquitectura. Antes de agregar compatibilidad con Hilt para ViewModels, migraremos los ViewModels de la app a los componentes de la arquitectura.

Para realizar la integración con ViewModel, debes agregar las siguientes dependencias adicionales al archivo de Gradle. En este caso, ya las agregamos por ti. Ten en cuenta que, además de la biblioteca, debes agregar un procesador de anotaciones adicional que funcione sobre el procesador de anotaciones 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 una clase simple a ViewModel, debes extender ViewModel().

Abre MainViewModel.kt y agrega : ViewModel(). Esto es suficiente para realizar la migración a los ViewModels de los componentes de arquitectura, pero también debes indicar a Hilt cómo proporcionar instancias del ViewModel. Para hacerlo, agrega la anotación @ViewModelInject en el constructor de ViewModel. Reemplaza la anotación @Inject con @ViewModelInject.

MainViewModel.kt

class MainViewModel @ViewModelInject constructor(
    private val userDataRepository: UserDataRepository
): ViewModel() {
//...
}

A continuación, abre LoginViewModel y realiza los mismos cambios. El código final debería verse así:

LoginViewModel.kt

class LoginViewModel @ViewModelInject constructor(
    private val userManager: UserManager
): ViewModel() {
//...
}

De manera similar, abre RegistrationViewModel.kt, migra a ViewModel() y agrega la anotación de Hilt. No necesitas la anotación @ActivityScoped, ya que con los métodos de extensión viewModels() y activityViewModels() puedes controlar el alcance de este ViewModel.

RegistrationViewModel.kt

class RegistrationViewModel @ViewModelInject constructor(
    val userManager: UserManager
) : ViewModel() {

Realiza los mismos cambios para migrar EnterDetailsViewModel y SettingViewModel. El código final para estas dos clases debe verse así:

EnterDetailsViewModel.kt

class EnterDetailsViewModel @ViewModelInject constructor() : ViewModel() {

SettingViewModel.kt

class SettingsViewModel @ViewModelInject constructor(
     private val userDataRepository: UserDataRepository,
     private val userManager: UserManager
) : ViewModel() {

Ahora que todos los ViewModels se migraron a los Viewmodels de componentes de la arquitectura y se anotaron con anotaciones de Hilt, puedes migrar la manera en que se insertan.

A continuación, debes cambiar el modo en que los ViewModels se inicializan en la capa View. El SO crea ViewModels y la forma de obtenerlos es mediante la función para delegar by viewModels().

Abre MainActivity.kt y reemplaza la anotación @Inject con las extensiones de Jetpack. Ten en cuenta que también debes quitar el lateinit, cambiar var por val y marcar el campo private.

MainActivity.kt

//    @Inject
//    lateinit var mainViewModel: MainViewModel
    private val mainViewModel: MainViewModel by viewModels()

Del mismo modo, abre LoginActivity.kt y cambia la forma en que se obtiene ViewModel.

LoginActivity.kt

//    @Inject
//    lateinit var loginViewModel: LoginViewModel
    private val loginViewModel: LoginViewModel by viewModels()

A continuación, abre RegistrationActivity.kt y aplica cambios similares para obtener el registrationViewModel.

RegistrationActivity.kt

//    @Inject
//    lateinit var registrationViewModel: RegistrationViewModel
    private val registrationViewModel: RegistrationViewModel by viewModels()

Abre EnterDetailsFragment.kt. Reemplaza la manera en la que se obtiene EnterDetailsViewModel.

EnterDetailsFragment.kt

    private val enterDetailsViewModel: EnterDetailsViewModel by viewModels()

De manera similar, reemplaza cómo se obtiene registrationViewModel, pero esta vez usa la función delegada activityViewModels(), en lugar de viewModels().. Cuando se inserta registrationViewModel, Hilt inserta el ViewModel específico a nivel de la actividad.

EnterDetailsFragment.kt

    private val registrationViewModel: RegistrationViewModel by activityViewModels()

Abre TermsAndConditionsFragment.kt y vuelve a usar la función de extensión activityViewModels(), en lugar de viewModels(), para obtener registrationViewModel.

TermsAndConditionsFragment.kt

    private val registrationViewModel: RegistrationViewModel by activityViewModels()

Por último, abre SettingsActivity.kt y migra la forma en que se obtiene settingsViewModel.

SettingsActivity.kt

    private val settingsViewModel: SettingsViewModel by viewModels()

Ahora ejecuta la app y confirma que todo funciona como se esperaba.

¡Felicitaciones! Migraste con éxito una app para usar Hilt. No solo completaste la migración, sino que también mantuviste la aplicación en funcionamiento mientras migrabas los componentes de Dagger uno por uno.

En este codelab, aprendiste a comenzar con el componente de la aplicación y a compilar las bases necesarias para que Hilt funcione con los componentes Dagger existentes. A partir de allí, migraste cada componente de Dagger a Hilt usando anotaciones de Hilt en actividades y fragmentos, y quitaste el código relacionado de Dagger. Cada vez que terminaste de migrar un componente, la app funcionó, y de la manera esperada. También migraste las dependencias Context y ApplicationContext con las anotaciones @ActivityContext y @ApplicationContext que proporciona Hilt. Luego, migraste otros componentes de Android y, por último, migraste las pruebas para completar el proceso.

Lecturas adicionales

Para obtener más información sobre cómo migrar tu app a Hilt, consulta la documentación sobre la migración a Hilt. Verás cómo migrar de Dagger a Hilt, y cómo migrar a dagger.android.app.