Hilt : Injection de dépendances sur Android
Chez V-labs, les projets les plus récents ont été créés en utilisant le principe d’inversion de contrôle. Divers outils ont été utilisés à cette fin : Dagger, Kodein et plus récemment Koin. L’inversion de contrôle, pour faire court, est la création de dépendances d’une classe en dehors de cette dernière ; les dépendances lui sont passées via le constructeur au moment de son instanciation. Ce faisant, les classes créées sont alors plus modulaires et plus faciles à tester.
Kodein et Koin utilisent le pattern de Service Locator, pour lequel une classe est chargée de transmettre les dépendances là où elles sont nécessaires. Dagger, de son côté, utilise l’injection de dépendances. Les dépendances sont alors résolues au build time. Forcément, cela rallonge le build, ce qui est souvent reproché à Dagger, mais cela permet néanmoins de se rendre compte des erreurs en amont, contrairement aux précédentes librairies pour lesquelles la résolution est au runtime et les erreurs de même.
Malgré cette sécurité proposée par Dagger, ce dernier est souvent mis de côté, et cela a été notre cas, du fait de sa complexité, de sa courbe d’apprentissage et de la quantité de code boilerplate nécessaire à son utilisation. Il est fastidieux de maîtriser ce framework, mais aussi de comprendre les erreurs de build.
Récemment, Google a annoncé une nouvelle librairie pour Jetpack, basée sur Dagger et ayant pour but de pallier ces problèmes. Hilt est actuellement en Alpha, mais on peut déjà voir une nette amélioration par rapport à Dagger.
Avec Hilt, il est toujours question d’utiliser les annotations afin de générer notre code, et de nouvelles annotations font forcément leur apparition, avec parmi les plus importantes :
- @HiltAndroidApp qui permet l’initialisation du framework et qui se placera forcément sur notre classe Application, évitant d’initialiser Dagger à la main.
- @AndroidEntryPoint qui permet d’injecter dans nos classes (Activity, Fragment, View, Services, BroadcastReceiver).
- @EntryPoint pour toutes les autres classes non supportées par la précédente annotation.
- @ViewModelInject : Comme pour Koin, Hilt se pare de son mot-clé pour les ViewModel.
On retrouve toujours @Inject qui permet l’injection de constructeur et de champs, et également @Provides dans nos modules pour les types ne pouvant pas être injectés via le constructeur (généralement toutes les dépendances externes au projet).
La plus grosse différence avec Dagger est la présence de composants prédéfinis. Tous ces composants auront pour but d’injecter les dépendances définies dans nos modules dans les classes annotées avec @AndroidEntryPoint/@EntryPoint.
On comprend aisément quel composant utiliser avec quel type de classe, sauf peut-être pour @ActivityRetainedComponent dont le but principal est de conserver les instances malgré les changements de configuration.
Comme pour Dagger, on retrouve toujours nos modules, à la différence près que l’on utilisera @InstallIn pour spécifier les composants dans lesquels installer le module.
Au sein de nos modules, il sera alors possible de préciser le scope, à savoir si l’on souhaite ou non conserver l’instance (Scoped) ou bien la recréer à chaque fois.
En clair, une instance notée @FragmentScoped sera toujours la même dans le fragment demandé ; cependant, elle sera bien évidemment différente dans un autre fragment. Afin de la rendre disponible dans tous les fragments, il sera nécessaire de changer le scope.
Voilà pour un tour succinct de Hilt, mais rien ne vaut la mise en pratique avec la migration de l’un de nos projets de Koin à Hilt pour se rendre compte des éventuels problèmes que l’on peut rencontrer.
La mise en place est plutôt simple, mais dès lors que l’on souhaite passer des interfaces, cela se complique. Il est alors nécessaire d’utiliser @Binds (comme auparavant avec Dagger) au lieu de @Provides, et notre module doit alors être abstrait, alors qu’auparavant il s’agissait d’un objet. Pour bien différencier chaque implémentation de notre interface, il est nécessaire d’utiliser un @Qualifier, une annotation personnalisée. Cette nouvelle annotation devra alors être utilisée au moment de l’injection dans notre classe pour spécifier l’implémentation attendue.
Extrait du codelab :
@Qualifier annotation class InMemoryLogger @Qualifier annotation class DatabaseLogger @InstallIn(ApplicationComponent::class) @Module abstract class LoggingDatabaseModule { @DatabaseLogger @Singleton @Binds abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource } @InstallIn(ActivityComponent::class) @Module abstract class LoggingInMemoryModule { @InMemoryLogger @ActivityScoped @Binds abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource } class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao){…} @AndroidEntryPoint class ButtonsFragment : Fragment() { @InMemoryLogger @Inject lateinit var logger: LoggerDataSource … }
Bon, jusque-là tout va bien. Le premier problème a été résolu facilement grâce aux codelabs que je vous invite fortement à suivre pour vous familiariser avec Hilt.
Finalement, ma migration s’est trouvée stoppée en raison de la nécessité d’injecter un paramètre dynamique dans mon constructeur de ViewModel. Pour le moment, à moins d’avoir raté cette information, Hilt, contrairement à Dagger, ne permet pas de le faire.
Forcément, en effectuant cette migration et en raison du manque de maîtrise de la librairie, j’ai pu commettre quelques erreurs. Cependant, contrairement à mes souvenirs de Dagger, la stack trace me semble nettement plus compréhensible et il est plus simple de déboguer l’application.
Hilt est encore en Alpha, mais il est clair que pour le moment les promesses sont tenues. L’utilisation de Dagger est nettement facilitée. De plus, du fait de son intégration à Jetpack, son évolution par rapport aux autres bibliothèques de Jetpack pourrait apporter plus de cohésion avec ces dernières. On attend avec impatience la release officielle pour en savoir plus sur ce que pourrait apporter Hilt dans notre processus de développement.