Migrer votre application Dagger vers Hilt

Dans cet atelier de programmation, vous allez apprendre comment migrer Dagger vers Hilt pour l'injection de dépendances dans une application Android. Nous nous basons sur le contenu de l'atelier Utiliser Dagger dans votre application Android pour cette migration. Le but est de vous présenter comment planifier votre migration pour que Dagger et Hilt s'exécutent en parallèle durant le processus, de sorte que l'appli continue de fonctionner pendant que vous migrez chaque composant Dagger vers Hilt.

L'injection de dépendances permet de réutiliser le code et de simplifier la refactorisation et les tests. Hilt repose sur la bibliothèque d'injection de dépendances Dagger et bénéficie ainsi de l'exactitude du temps de compilation, des performances d'exécution, de l'évolutivité et de la compatibilité avec Android Studio qu'offre Dagger.

Étant donné que de nombreuses classes du framework Android sont instanciées par le système d'exploitation, lorsque vous utilisez Dagger dans des applis Android, il en résulte quantité de code récurrent. Hilt supprime la majeure partie de ce code en générant et en fournissant automatiquement :

  • des composants permettant d'intégrer des classes du framework Android avec Dagger, qu'il faudrait sinon créer manuellement ;
  • des annotations de champ d'application pour les composants générés automatiquement par Hilt ;
  • des liaisons et qualificatifs prédéfinis.

De plus, comme Dagger et Hilt peuvent coexister, vous pouvez migrer vos applications selon les besoins.

Si vous rencontrez des problèmes (bugs de code, erreurs grammaticales, formulation peu claire, etc.) au cours de cet atelier de programmation, veuillez les signaler via le lien "Signaler une erreur" situé dans l'angle inférieur gauche de l'atelier de programmation.

Conditions préalables

  • Connaissance pratique de la syntaxe Kotlin
  • Connaissance pratique de Dagger

Points abordés

  • Ajouter Hilt à votre application Android
  • Planifier votre stratégie de migration
  • Migrer des composants vers Hilt tout en préservant le fonctionnement du code Dagger existant
  • Migrer des composants restreints
  • Tester votre application avec Hilt

Prérequis

  • Android Studio version 4.0 ou ultérieure

Obtenir le code

Obtenez le code de l'atelier de programmation sur GitHub :

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

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP :

Télécharger le fichier ZIP

Ouvrir Android Studio

Si vous devez télécharger Android Studio, cliquez ici.

Configuration du projet

Le projet comporte plusieurs branches GitHub :

  • master représente la branche que vous avez extraite ou téléchargée. Elle est le point de départ de cet atelier de programmation.
  • interop correspond à la branche d'interopérabilité entre Dagger et Hilt.
  • solution contient le code de la solution de l'atelier de programmation, y compris les tests et les ViewModels.

Nous vous recommandons de suivre l'atelier de programmation étape par étape, à votre propre rythme, en commençant par la branche master.

Au cours de cet atelier de programmation, vous découvrirez des extraits de code que vous devrez ajouter au projet. À certains endroits, vous devrez également supprimer le code qui est explicitement mentionné dans les commentaires sur les extraits de code.

Si vous avez besoin d'aide pour une étape particulière, vous pouvez utiliser les branches intermédiaires comme points de contrôle.

Pour obtenir la branche solution à l'aide de Git, exécutez la commande suivante :

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

Vous pouvez également télécharger le code de la solution en cliquant sur le bouton suivant :

Télécharger le code final

Questions fréquentes

Exécuter l'exemple d'application

Voyons d'abord comment se présente notre exemple d'application à l'état d'origine. Suivez les instructions ci-dessous pour ouvrir l'exemple d'application dans Android Studio :

  • Si vous avez téléchargé l'archive ZIP, décompressez le fichier localement.
  • Ouvrez le projet dans Android Studio.
  • Cliquez sur le bouton Exécuter execute.png, puis sélectionnez un émulateur ou connectez votre appareil Android. L'écran d'inscription devrait s'afficher.

54d4e2a9bf8177c1.gif

L'appli se compose de quatre flux différents fonctionnant avec Dagger et se présentant sous la forme d'activités :

  • Registration (Inscription) : l'utilisateur peut s'inscrire en saisissant son nom d'utilisateur et son mot de passe et en acceptant les conditions d'utilisation.
  • Login (Connexion) : l'utilisateur peut se connecter à l'aide des identifiants choisis pendant le flux d'inscription ou encore se désinscrire de l'application.
  • Home (Accueil) : l'utilisateur accède à la page d'accueil qui affiche le nombre de notifications non lues.
  • Settings (Paramètres) : l'utilisateur peut se déconnecter et actualiser le nombre de notifications non lues, ce qui génère un nombre aléatoire de notifications.

Le projet suit un modèle MVVM classique, où toute la complexité de la vue est reportée à un ViewModel. Prenez quelques instants pour vous familiariser avec la structure du projet.

8ecf1f9088eb2bb6.png

Les flèches représentent les dépendances entre les objets. Ce graphique d'application, comme on l'appelle, montre toutes les classes de l'application et les dépendances entre elles.

Le code de la branche master utilise Dagger pour injecter des dépendances. Au lieu de créer manuellement des composants, nous allons refactoriser l'application pour qu'elle utilise Hilt afin de générer des composants ou tout autre code associé à Dagger.

Dagger est configuré dans l'appli comme illustré dans le schéma suivant. La présence d'un point signifie que le type d'élément est limité au composant qui le fournit.

a1b8656d7fc17b7d.png

Pour plus de simplicité, les dépendances de Hilt sont déjà ajoutées au projet dans la branche master que vous avez téléchargée au début. Vous n'avez pas besoin d'ajouter le code suivant dans votre projet, car cela a déjà été fait. Passons toutefois en revue les éléments nécessaires pour utiliser Hilt dans une application Android.

Outre les dépendances de bibliothèque, Hilt utilise un plug-in Gradle qui est configuré pour le projet. Ouvrez le fichier racine build.gradle (au niveau du projet) et recherchez la dépendance Hilt suivante dans le chemin de classe :

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

Ouvrez app/build.gradle et vérifiez la déclaration du plug-in Gradle de Hilt en haut de la page, juste en dessous du plug-in kotlin-kapt.

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

android {
    ...
}

Enfin, les dépendances de Hilt et le processeur d'annotations sont inclus dans notre projet, dans le même fichier app/build.gradle :

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

Toutes les bibliothèques, y compris Hilt, sont téléchargées lorsque vous créez et synchronisez le projet. Commençons maintenant à utiliser Hilt !

Vous pourriez être tenté de tout migrer vers Hilt en même temps. Cependant, dans un projet réel, il convient d'effectuer une migration progressive afin de vous assurer que la création et l'exécution de l'appli se déroulent correctement.

Vous devez donc décomposer la migration vers Hilt en plusieurs étapes. La méthode recommandée consiste à commencer par migrer votre application ou le composant @Singleton, puis les activités et les fragments.

Dans cet atelier de programmation, vous allez d'abord migrer AppComponent, puis chaque flux de l'application en commençant par l'inscription, pour enchaîner avec la connexion, le flux principal et les paramètres.

Pendant la migration, vous devez supprimer toutes les interfaces @Component et @Subcomponent, et annoter tous les modules avec @InstallIn.

Une fois celle-ci terminée, toutes les classes Application/Activity/Fragment/View/Service/BroadcastReceiver doivent être annotées avec @AndroidEntryPoint, et tout composant d'instanciation ou de propagation du code doit également être supprimé.

Pour planifier la migration, commençons par AppComponent.kt afin de mieux comprendre la hiérarchie des composants.

@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 est annoté avec @Component et comprend deux modules, StorageModule et AppSubcomponents.

AppSubcomponents comprend trois composants, RegistrationComponent, LoginComponent et UserComponent.

  • LoginComponent est injecté dans LoginActivity.
  • RegistrationComponent est injecté dans RegistrationActivity, EnterDetailsFragment et TermsAndConditionsFragment. Ce composant est également limité à RegistrationActivity.

UserComponent est injecté dans MainActivity et SettingsActivity.

Les références à ApplicationComponent peuvent être remplacées par le composant généré par Hilt (lien vers tous les composants générés) qui est mappé sur le composant que vous migrez dans votre application.

Dans cette section, vous allez migrer AppComponent. Il s'agit de préparer le terrain pour que le code Dagger existant continue de s'exécuter correctement pendant que vous migrez vers Hilt chaque composant de l'application.

Pour initialiser Hilt et démarrer la génération du code, vous devez annoter la classe Application avec des annotations Hilt.

Ouvrez MyApplication.kt et ajoutez l'annotation @HiltAndroidApp à la classe. Ces annotations indiquent à Hilt de déclencher la génération du code que Dagger détectera et utilisera dans son processeur d'annotations.

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. Migrez les modules de composants

Pour commencer, ouvrez AppComponent.kt. AppComponent comporte deux modules (StorageModule et AppSubcomponents), qui sont ajoutés dans l'annotation @Component. Vous devez tout d'abord migrer ces deux modules, afin que Hilt les ajoute dans le fichier ApplicationComponent généré.

Pour ce faire, ouvrez AppSubcomponents.kt et annotez la classe avec l'annotation @InstallIn. L'annotation @InstallIn utilise un paramètre pour ajouter le module au bon composant. Dans notre cas, lorsque vous migrez le composant au niveau de l'application, les liaisons doivent être générées dans 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

Vous devez effectuer la même modification dans StorageModule. Ouvrez StorageModule.kt et ajoutez l'annotation @InstallIn comme dans l'étape précédente.

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
}

Avec l'annotation @InstallIn, vous avez à nouveau demandé à Hilt d'ajouter le module au composant d'application généré par Hilt.

Revenons maintenant au fichier AppComponent.kt. AppComponent fournit des dépendances pour RegistrationComponent, LoginComponent et UserManager. Au cours des étapes suivantes, vous allez préparer ces composants pour la migration.

2. Migrez les types exposés

Pendant que vous migrez entièrement l'application vers Hilt, vous pouvez demander manuellement des dépendances à Dagger en utilisant des points d'entrée. Les points d'entrée vous permettent de vous assurer que l'application continuera de fonctionner pendant la migration de chaque composant Dagger. Au cours de cette étape, vous allez remplacer chaque composant Dagger par une recherche manuelle de dépendances dans le fichier ApplicationComponent généré par Hilt.

Pour obtenir RegistrationComponent.Factory pour RegistrationActivity.kt à partir du fichier ApplicationComponent généré par Hilt, vous devez créer une nouvelle interface EntryPoint annotée avec @InstallIn. L'annotation InstallIn indique à Hilt d'où il doit obtenir la liaison. Pour accéder à un point d'entrée, utilisez la méthode statique appropriée depuis EntryPointAccessors. Le paramètre doit correspondre soit à l'instance du composant, soit à l'objet @AndroidEntryPoint qui agit comme le conteneur des composants.

RegistrationActivity.kt

class RegistrationActivity : AppCompatActivity() {

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

    ...
}

Vous devez maintenant remplacer le code associé à Dagger par RegistrationEntryPoint. Modifiez l'initialisation de registrationComponent pour qu'il utilise RegistrationEntryPoint. Avec cette modification, RegistrationActivity peut accéder à ses dépendances en utilisant le code généré par Hilt jusqu'à ce qu'il soit migré vers 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()

Vous devez ensuite faire de même pour tous les autres types de composants exposés. Continuons avec LoginComponent.Factory. Ouvrez LoginActivity et créez une interface LoginEntryPoint annotée avec @InstallIn et @EntryPoint, comme vous l'avez fait précédemment, cette fois en exposant les éléments que LoginActivity nécessite du composant Hilt.

LoginActivity.kt

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

Maintenant que Hilt sait comment fournir LoginComponent, remplacez l'ancien appel inject() par l'appel loginComponent() du point d'entrée.

LoginActivity.kt

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

Deux des trois types exposés de AppComponent sont remplacés par des points d'entrée Hilt. Vous devez ensuite effectuer le même type de modification pour UserManager. Contrairement à RegistrationComponent et à LoginComponent, UserManager est utilisé à la fois dans MainActivity et SettingsActivity. Vous n'avez besoin de créer une interface EntryPoint qu'une seule fois. L'interface EntryPoint annotée peut être utilisée dans les deux activités. Pour plus de simplicité, déclarez l'interface dans MainActivity.

Pour créer une interface UserManagerEntryPoint, ouvrez le fichier MainActivity.kt et annotez-le avec @InstallIn et @EntryPoint.

MainActivity.kt

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

Maintenant, modifiez UserManager de sorte qu'il utilise UserManagerEntryPoint.

MainActivity.kt

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

Vous devez effectuer la même modification dans le fichier SettingsActivity. Ouvrez SettingsActivity.kt et remplacez le processus d'injection de UserManager.

SettingsActivity.kt

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

3. Supprimez le composant Factory

Une méthode courante consiste à transmettre l'argument Context à un composant Dagger avec l'annotation @BindsInstance. Cela n'est pas nécessaire dans Hilt, car Context est déjà disponible en tant que liaison prédéfinie.

En règle générale, Context est nécessaire pour accéder aux ressources, aux bases de données, aux préférences partagées, etc. Hilt simplifie l'injection de contexte en utilisant les qualificatifs @ApplicationContext et @ActivityContext.

Lors de la migration de votre appli, vérifiez quels types nécessitent Context en tant que dépendance et remplacez-les par ceux fournis par Hilt.

Dans notre cas, SharedPreferencesStorage utilise Context comme dépendance. Pour indiquer à Hilt d'injecter le contexte, ouvrez SharedPreferencesStorage.kt. SharedPreferences nécessite l'extension Context de l'application. Vous devez donc ajouter l'annotation @ApplicationContext au paramètre de contexte.

SharedPreferencesStorage.kt

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

//...

4. Migrez les méthodes d'injection

Ensuite, vous devez vérifier la présence de méthodes inject() dans le code du composant et annoter les classes correspondantes avec @AndroidEntryPoint. Dans notre cas, il n'existe aucune méthode inject() pour AppComponent. Vous n'avez donc rien à faire.

5. Supprimez la classe AppComponent

Puisque vous avez déjà ajouté des points d'entrée pour tous les composants répertoriés dans AppComponent.kt, vous pouvez supprimer AppComponent.kt.

6. Supprimez le code qui utilise le composant pour effectuer la migration

Vous n'avez plus besoin du code pour initialiser l'AppComponent personnalisé dans la classe Application. Celle-ci utilise la classe ApplicationComponent générée par Hilt. Supprimez tout le code présent dans le corps de la classe. Le code final doit ressembler à l'exemple ci-dessous :

MyApplication.kt

package com.example.android.dagger

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

@HiltAndroidApp
open class MyApplication : Application()

Dans cette section, vous avez ajouté Hilt à votre application, supprimé AppComponent et modifié le code Dagger pour qu'il injecte des dépendances en utilisant le composant AppComponent généré par Hilt. Lorsque vous compilez et testez l'application sur un appareil ou un émulateur, elle doit fonctionner comme avant. Dans les sections suivantes, nous allons migrer les activités (Activity) et les fragments (Fragment) vers Hilt.

Maintenant que vous avez migré le composant Application et préparé le terrain, vous pouvez migrer vers Hilt chaque composant un par un.

Commençons par migrer le flux de connexion. Au lieu de créer LoginComponent manuellement et de l'utiliser dans LoginActivity, vous souhaitez que Hilt le fasse pour vous.

Vous pouvez suivre la procédure décrite dans la section précédente, mais en utilisant le composant ActivityComponent généré par Hilt, car il s'agit de migrer un composant géré par une activité.

Pour commencer, ouvrez LoginComponent.kt. LoginComponent ne comporte aucun module. Aucune action de votre part n'est donc requise. Pour que Hilt génère un composant pour LoginActivity et l'injecte, vous devez annoter l'activité avec @AndroidEntryPoint.

LoginActivity.kt

@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {

    //...
}

Pour migrer LoginActivity vers Hilt, vous devez simplement ajouter le code ci-dessus. Puisque Hilt génère le code associé à Dagger, il suffit d'effectuer un nettoyage de code. Supprimez l'interface LoginEntryPoint.

LoginActivity.kt

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

Supprimez ensuite le code du point d'entrée dans 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)

    ...
}

Puisque Hilt va générer le composant, recherchez et supprimez LoginComponent.kt.

LoginComponent est actuellement répertorié en tant que sous-composant dans AppSubcomponents.kt. Vous pouvez supprimer LoginComponent de la liste de sous-composants en toute sécurité, car Hilt générera les liaisons pour vous.

AppSubcomponents.kt

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

Voilà tout qu'il vous faut pour migrer LoginActivity vers Hilt. Dans cette section, vous avez supprimé plus de code que vous n'en avez ajouté, ce qui est très bien. Avec Hilt, vous avez moins de code à saisir et à maintenir, ce qui limite le risque d'introduire des bugs.

Dans cette section, vous allez migrer le flux d'inscription. Pour planifier la migration, examinez RegistrationComponent. Ouvrez RegistrationComponent.kt et faites défiler la page jusqu'à la fonction inject(). RegistrationComponent est chargé d'injecter des dépendances à RegistrationActivity, EnterDetailsFragment et TermsAndConditionsFragment.

Commençons par migrer RegistrationActivity. Ouvrez RegistrationActivity.kt et annotez la classe avec @AndroidEntryPoint.

RegistrationActivity.kt

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

Maintenant que RegistrationActivity est enregistré dans Hilt, vous pouvez supprimer de la fonction onCreate() l'interface RegistrationEntryPoint et le code associé au point d'entrée.

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 est chargé de générer le composant et d'injecter des dépendances. Vous pouvez donc supprimer la variable registrationComponent et l'appel d'injection du composant Dagger supprimé.

RegistrationActivity.kt

// Remove
// lateinit var registrationComponent: RegistrationComponent

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

    //..
}

Ensuite, ouvrez EnterDetailsFragment.kt. Annotez EnterDetailsFragment avec @AndroidEntryPoint, comme vous l'avez fait pour RegistrationActivity.

EnterDetailsFragment.kt

@AndroidEntryPoint
class EnterDetailsFragment : Fragment() {

    //...
}

Puisque Hilt fournit les dépendances, l'appel inject() du composant Dagger supprimé est superflu. Supprimez la fonction onAttach().

L'étape suivante consiste à migrer TermsAndConditionsFragment. Ouvrez TermsAndConditionsFragment.kt, annotez la classe et supprimez la fonction onAttach() comme vous l'avez fait à l'étape précédente. Le code final doit se présenter comme suit :

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

Avec cette modification, vous avez migré l'ensemble des activités et des fragments répertoriés dans RegistrationComponent. Vous pouvez donc supprimer RegistrationComponent.kt.

Après avoir supprimé RegistrationComponent, vous devez supprimer également sa référence de la liste des sous-composants dans AppSubcomponents.

AppSubcomponents.kt

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

Il ne reste qu'une dernière étape pour terminer la migration du flux d'inscription. Le flux d'inscription déclare et utilise son propre champ d'application, ActivityScope. Les champs d'application contrôlent le cycle de vie des dépendances. Dans notre cas, ActivityScope indique à Dagger d'injecter la même instance de RegistrationViewModel dans le flux lancé par RegistrationActivity. Hilt fournit des champs d'application de cycle de vie intégrés.

Ouvrez RegistrationViewModel et remplacez l'annotation @ActivityScope par l'annotation @ActivityScoped fournie par Hilt.

RegistrationViewModel.kt

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

    //...
}

Puisque ActivityScope n'est utilisé nulle part ailleurs, vous pouvez supprimer le fichier ActivityScope.kt en toute sécurité.

Maintenant, exécutez l'application et testez le flux d'inscription. Vous pouvez utiliser votre nom d'utilisateur et votre mot de passe actuels pour vous connecter, ou vous désinscrire et créer un nouveau compte, afin de vérifier que le flux fonctionne comme avant.

Désormais, Dagger et Hilt fonctionnent en parallèle dans l'application. Hilt injecte toutes les dépendances, sauf UserManager. Dans la section suivante, vous allez terminer la migration de Dagger vers Hilt en migrant UserManager.

Dans cet atelier de programmation, jusqu'à présent, vous avez migré vers Hilt l'intégralité de l'exemple d'application, à l'exception d'un seul composant : UserComponent. UserComponent est annoté avec un champ d'application personnalisé, @LoggedUserScope. Cela signifie que UserComponent injecte la même instance UserManager dans les classes annotées avec @LoggedUserScope.

UserComponent ne mappe aucun des composants Hilt disponibles, car son cycle de vie n'est pas géré par une classe Android. Comme il n'est pas possible d'ajouter un composant personnalisé au milieu de la hiérarchie Hilt générée, deux options s'offrent à vous :

  1. Laisser Hilt et Dagger fonctionner en parallèle dans l'état où se trouve le projet actuellement
  2. Migrer le composant restreint vers le composant Hilt le plus proche (dans notre cas, ApplicationComponent) et utiliser la valeur "null" si nécessaire

Vous avez déjà atteint la première étape dans la section précédente. Ici, vous suivrez la deuxième étape pour compléter la migration de l'application vers Hilt. Dans une application réelle, vous êtes libre de choisir l'option qui convient le mieux à votre cas d'utilisation.

Dans cette étape, UserComponent sera migré pour faire partie du ApplicationComponent de Hilt. Si ce composant contient un ou plusieurs modules, ceux-ci doivent également être installés dans ApplicationComponent.

Le seul type restreint dans UserComponent est UserDataRepository, qui est annoté avec @LoggedUserScope. Comme UserComponent sera fusionné avec le ApplicationComponent de Hilt, UserDataRepository sera annoté avec @Singleton et vous modifierez la logique pour que la valeur soit "null" lorsque l'utilisateur se déconnecte.

UserManager est déjà annoté avec @Singleton, ce qui signifie que vous pouvez fournir la même instance pour toute l'application et, en apportant quelques modifications, obtenir la même fonctionnalité avec Hilt. Commencez par modifier le fonctionnement de UserManager et de UserDataRepository, car vous devez d'abord préparer le terrain.

Ouvrez UserManager.kt et appliquez les modifications suivantes.

  • Remplacez le paramètre UserComponent.Factory par UserDataRepository dans le constructeur, car vous n'avez plus besoin de créer une instance de UserComponent. Le fichier a désormais UserDataRepository comme dépendance.
  • Puisque Hilt générera le code du composant, supprimez UserComponent et son setter.
  • Modifiez la fonction isUserLoggedIn() pour qu'elle vérifie le nom d'utilisateur en utilisant userRepository au lieu de userComponent.
  • Ajoutez le nom d'utilisateur en tant que paramètre dans la fonction userJustLoggedIn().
  • Modifiez le corps de la fonction userJustLoggedIn() pour qu'elle appelle initData avec userName sur userDataRepository (et non sur userComponent, que vous supprimerez lors de la migration).
  • Ajoutez username à l'appel userJustLoggedIn() dans les fonctions registerUser() et loginUser().
  • Supprimez userComponent de la fonction logout() et remplacez-le par un appel vers userDataRepository.cleanUp().

Lorsque vous avez terminé, le code final de UserManager.kt doit ressembler à ce qui suit :

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

Maintenant que vous avez terminé avec UserManager, vous devez apporter quelques modifications dans UserDataRepository. Ouvrez UserDataRepository.kt et appliquez les modifications suivantes :

  • Supprimez @LoggedUserScope, car cette dépendance sera gérée par Hilt.
  • UserDataRepository est déjà injecté dans UserManager. Pour éviter une dépendance cyclique, supprimez le paramètre UserManager du constructeur de UserDataRepository.
  • Redéfinissez le paramètre unreadNotifications sur la valeur "null" et rendez le setter privé.
  • Ajoutez une nouvelle variable username pouvant être vide et rendez le setter privé.
  • Ajoutez une fonction initData() qui définit les valeurs username et unreadNotifications sur un nombre aléatoire.
  • Ajoutez une fonction cleanUp() pour réinitialiser le compteur de username et de unreadNotifications. Définissez username sur la valeur "null" et unreadNotifications sur -1.
  • Enfin, déplacez la fonction randomInt() dans le corps de la classe.

Lorsque vous avez terminé, le code final doit ressembler à ce qui suit :

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

Pour terminer la migration de UserComponent, ouvrez UserComponent.kt et faites défiler la page jusqu'aux méthodes inject(). Cette dépendance est utilisée dans MainActivity et SettingsActivity. Commençons par migrer MainActivity. Ouvrez MainActivity.kt et annotez la classe avec @AndroidEntryPoint.

MainActivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    //...
}

Supprimez l'interface UserManagerEntryPoint et le code associé au point d'entrée 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)

    //...
}

Déclarez une variable lateinit var pour UserManager et ajoutez-y une annotation @Inject afin que Hilt puisse injecter la dépendance.

MainActivity.kt

@Inject
lateinit var userManager: UserManager

Puisque UserManager sera injecté par Hilt, supprimez l'appel inject() dans UserComponent.

MainActivity.kt

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

Voilà tout ce que vous avez à faire pour MainActivity. Vous pouvez maintenant effectuer des modifications similaires pour migrer SettingsActivity. Ouvrez SettingsActivity et ajoutez une annotation avec @AndroidEntryPoint.

SettingsActivity.kt

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

Créez une variable lateinit var pour UserManager et ajoutez-y une annotation avec @Inject.

SettingsActivity.kt

    @Inject
    lateinit var userManager: UserManager

Supprimez le code de point d'entrée et l'appel d'injection vers userComponent(). Lorsque vous avez terminé, la fonction onCreate() doit ressembler à ce qui suit :

SettingsActivity.kt

    override fun onCreate(savedInstanceState: Bundle?) {

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

        setupViews()
    }

Vous pouvez maintenant nettoyer les ressources inutilisées pour terminer la migration. Supprimez les classes LoggedUserScope.kt et UserComponent.kt, puis AppSubcomponent.kt.

Exécutez et testez à nouveau l'application. Celle-ci devrait fonctionner comme avant avec Dagger.

Il vous reste une étape essentielle avant de terminer la migration de l'application vers Hilt. Jusqu'à présent, vous avez migré l'ensemble du code de l'application, mais pas les tests. Hilt injecte des dépendances dans les tests comme il le fait dans le code de l'application. Avec Hilt, les tests ne nécessitent aucune maintenance, car Hilt génère automatiquement un nouvel ensemble de composants pour chaque test.

Tests unitaires

Commençons par les tests unitaires. Vous n'avez pas besoin d'utiliser Hilt pour les tests unitaires, car vous pouvez appeler directement le constructeur de la classe cible en définissant des dépendances factices ou fictives comme vous l'auriez fait si celui-ci n'était pas annoté.

Si vous exécutez les tests unitaires, vous constaterez que UserManagerTest échoue. Dans les sections précédentes, vous avez fait beaucoup de travail et apporté des modifications dans UserManager, y compris ses paramètres de constructeur. Ouvrez UserManagerTest.kt, qui dépend toujours de UserComponent et de UserComponentFactory. Puisque vous avez déjà modifié les paramètres de UserManager, remplacez le paramètre UserComponent.Factory par une nouvelle instance de UserDataRepository.

TestManagerManager.kt

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

C'est tout ! Relancez les tests unitaires. Ils doivent tous réussir.

Ajouter des dépendances de test

Avant de vous lancer, ouvrez app/build.gradle et vérifiez la présence des dépendances de Hilt suivantes. Hilt utilise hilt-android-testing pour les annotations propres aux tests. De plus, comme Hilt doit générer du code pour les classes du dossier androidTest, son processeur d'annotations doit pouvoir s'exécuter à cet emplacement.

app/build.gradle

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

Tests de l'interface utilisateur

Pour chaque test, Hilt génère automatiquement des composants de test et une application de test. Pour commencer, ouvrez TestAppComponent.kt afin de planifier la migration. TestAppComponent comporte deux modules : TestStorageModule et AppSubcomponents. Vous avez déjà migré et supprimé AppSubcomponents, vous pouvez donc effectuer la migration de TestStorageModule.

Ouvrez TestStorageModule.kt et annotez la classe avec @InstallIn.

TestStorageModule.kt

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

Puisque vous avez terminé la migration de tous les modules, vous pouvez supprimer TestAppComponent.

Ajoutons maintenant Hilt à ApplicationTest. Vous devez annoter avec @HiltAndroidTest tout test de l'interface utilisateur qui utilise Hilt. Cette annotation permet de générer les composants Hilt pour chaque test.

Ouvrez ApplicationTest.kt et ajoutez les annotations suivantes :

  • @HiltAndroidTest pour indiquer à Hilt de générer des composants pour ce test.
  • @UninstallModules(StorageModule::class) pour indiquer à Hilt de désinstaller le StorageModule déclaré dans le code de l'application de sorte que, lors des tests, TestStorageModule soit injecté à sa place.
  • Vous devez également ajouter une règle de test HiltAndroidRule à ApplicationTest. Celle-ci gère l'état des composants et permet d'effectuer une injection sur votre test. Le code final doit se présenter comme suit :

ApplicationTest.kt

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

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    //...

Puisque Hilt génère une nouvelle classe Application pour chaque test d'instrumentation, vous devez spécifier que Application doit être utilisé pour exécuter des tests de l'interface utilisateur. Pour ce faire, vous avez besoin d'un lanceur de test personnalisé.

L'application de l'atelier de programmation en a déjà un. Ouvrez MyCustomTestRunner.kt.

Hilt intègre une Application (intitulée HiltTestApplication.) que vous pouvez utiliser pour les tests. Pour cela, vous devez remplacer MyTestApplication::class.java par HiltTestApplication::class.java dans le corps de la fonction 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)
    }
}

Cette modification permet désormais de supprimer le fichier MyTestApplication.kt en toute sécurité. Vous pouvez à présent exécuter les tests. Tous les tests doivent réussir.

Hilt inclut des extensions permettant de fournir des classes issues d'autres bibliothèques Jetpack, telles que WorkManager et ViewModel. Les ViewModels du projet de l'atelier de programmation sont des classes simples qui ne complètent pas le ViewModel des composants d'architecture. Avant d'activer la compatibilité de Hilt pour les ViewModels, nous allons migrer les ViewModels de l'application vers ceux des composants d'architecture.

Pour l'intégration à ViewModel, vous devez ajouter les dépendances supplémentaires suivantes à votre fichier Gradle. Ces dépendances ont déjà été ajoutées pour vous. Notez qu'en plus de la bibliothèque, vous devez ajouter un processeur d'annotations supplémentaire qui s'exécute au-dessus de celui de 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'
}

Pour migrer une classe simple vers ViewModel, vous devez compléter ViewModel().

Ouvrez MainViewModel.kt et ajoutez : ViewModel(). Si cela suffit pour la migration vers les VIewModels des composants d'architecture, vous devez également indiquer à Hilt comment fournir des instances de la classe ViewModel. Pour ce faire, ajoutez l'annotation @ViewModelInject au constructeur de ViewModel. Remplacez l'annotation @Inject par @ViewModelInject.

MainViewModel.kt

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

Ensuite, ouvrez LoginViewModel et appliquez les mêmes modifications. Le code final doit se présenter comme suit :

LoginViewModel.kt

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

De même, ouvrez RegistrationViewModel.kt, effectuez la migration vers ViewModel() et ajoutez l'annotation de Hilt. Vous n'avez pas besoin de l'annotation @ActivityScoped, car les méthodes d'extension viewModels() et activityViewModels() permettent de contrôler le champ d'application de ce ViewModel.

RegistrationViewModel.kt

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

Apportez les mêmes modifications pour migrer EnterDetailsViewModel et SettingViewModel. Le code final pour ces deux classes doit se présenter comme suit :

EnterDetailsViewModel.kt

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

SettingViewModel.kt

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

Maintenant que tous les ViewModels sont migrés vers ceux des composants d'architecture et annotés avec des annotations Hilt, vous pouvez migrer leur méthode d'injection.

Ensuite, vous devez modifier la façon dont les ViewModels sont initialisés dans la couche View. Les ViewModels sont créés par le système d'exploitation. Pour les obtenir, vous devez utiliser la fonction de délégation by viewModels().

Ouvrez MainActivity.kt et remplacez l'annotation @Inject par les extensions Jetpack. Notez que vous devez également supprimer lateinit, remplacer var par val et marquer ce champ comme private.

MainActivity.kt

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

Comme avant, ouvrez LoginActivity.kt et modifiez la façon dont le ViewModel est obtenu.

LoginActivity.kt

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

Ensuite, ouvrez RegistrationActivity.kt et appliquez des modifications similaires pour obtenir le registrationViewModel.

RegistrationActivity.kt

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

Ouvrez EnterDetailsFragment.kt. Remplacez la façon dont EnterDetailsViewModel est obtenu.

EnterDetailsFragment.kt

    private val enterDetailsViewModel: EnterDetailsViewModel by viewModels()

De même, remplacez la façon dont registrationViewModel est obtenu, mais en utilisant la fonction de délégation activityViewModels() au lieu de viewModels().. Lorsque registrationViewModel est injecté, Hilt injecte le ViewModel limité au niveau de l'activité.

EnterDetailsFragment.kt

    private val registrationViewModel: RegistrationViewModel by activityViewModels()

Ouvrez TermsAndConditionsFragment.kt et utilisez encore une fois la fonction d'extension activityViewModels() au lieu de viewModels() pour obtenir registrationViewModel..

TermesAndConditionsFragment.kt

    private val registrationViewModel: RegistrationViewModel by activityViewModels()

Enfin, ouvrez le paramètre SettingsActivity.kt et migrez la façon dont settingsViewModel est obtenu.

SettingsActivity.kt

    private val settingsViewModel: SettingsViewModel by viewModels()

Maintenant, exécutez l'application et vérifiez que tout fonctionne comme prévu.

Félicitations ! Vous avez migré une application vers Hilt et vous vous êtes assuré que celle-ci continuait de fonctionner pendant la migration des composants Dagger un par un.

Dans cet atelier de programmation, vous avez appris à utiliser le composant d'application et à préparer le terrain pour que Hilt puisse fonctionner avec les composants Dagger existants. Ensuite, vous avez migré vers Hilt chaque composant Dagger, en utilisant des annotations Hilt sur les activités et les fragments, et en supprimant le code associé à Dagger. Après avoir migré chaque composant, vous avez pu constater que l'application fonctionnait comme prévu. Vous avez également migré les dépendances Context et ApplicationContext, à l'aide des annotations @ActivityContext et @ApplicationContext fournies par Hilt. Vous avez effectué la migration d'autres composants Android. Enfin, vous avez migré les tests et terminé la migration vers Hilt.

Complément d'informations

Pour en savoir plus sur la migration de votre application vers Hilt, consultez la documentation de migration vers Hilt. Vous y trouverez des informations sur la migration de Dagger vers Hilt, mais aussi sur la migration d'une application dagger.android.