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 :
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 :
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 , puis sélectionnez un émulateur ou connectez votre appareil Android. L'écran d'inscription devrait s'afficher.
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.
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.
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é dansLoginActivity
.RegistrationComponent
est injecté dansRegistrationActivity
,EnterDetailsFragment
etTermsAndConditionsFragment
. 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 :
- Laisser Hilt et Dagger fonctionner en parallèle dans l'état où se trouve le projet actuellement
- 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
parUserDataRepository
dans le constructeur, car vous n'avez plus besoin de créer une instance deUserComponent
. Le fichier a désormaisUserDataRepository
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 utilisantuserRepository
au lieu deuserComponent
. - Ajoutez le nom d'utilisateur en tant que paramètre dans la fonction
userJustLoggedIn()
. - Modifiez le corps de la fonction
userJustLoggedIn()
pour qu'elle appelleinitData
avecuserName
suruserDataRepository
(et non suruserComponent
, que vous supprimerez lors de la migration). - Ajoutez
username
à l'appeluserJustLoggedIn()
dans les fonctionsregisterUser()
etloginUser()
. - Supprimez
userComponent
de la fonctionlogout()
et remplacez-le par un appel versuserDataRepository.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é dansUserManager
. Pour éviter une dépendance cyclique, supprimez le paramètreUserManager
du constructeur deUserDataRepository
.- 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 valeursusername
etunreadNotifications
sur un nombre aléatoire. - Ajoutez une fonction
cleanUp()
pour réinitialiser le compteur deusername
et deunreadNotifications
. Définissezusername
sur la valeur "null" etunreadNotifications
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 leStorageModule
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.