[JAVA] Dagger Hilt (document DevFest 2020)

Ceci est une version article du matériel DevFest. Je vais expliquer chaque étape avec la différence de l'exemple d'application. Nous allons commencer par expliquer pourquoi vous avez besoin d'injection de dépendances, expliquer Dagger Hilt et vous entraîner!

Qu'est-ce que l'injection de dépendance (DI)?

Pourquoi vous avez besoin de DI

DI a une image un peu difficile, mais pourquoi est-ce nécessaire en premier lieu? Disons que vous créez une application qui lit des vidéos et que vous avez une classe appelée VideoPlayer. La base de données, le codec, etc. sont ** codés en dur dans la classe VideoPlayer. ** **

Code: https://github.com/takahirom/hilt-sample-app/commit/8c36602aaa4e27d8f10c81e2808f0ff452f1c8a4#diff-bbc9d28d8bcbd080a704cacc92b8cf37R19

class VideoPlayer {
    //Une liste de vidéos est stockée dans la base de données(J'utilise une bibliothèque appelée Room)
    private val database = Room
        .databaseBuilder(
            App.instance,
            VideoDatabase::class.java,
            "database"
        )
        .createFromAsset("videos.db")
        .build()
    //Liste des codecs utilisables
    private val codecs = listOf(FMP4, WebM, MPEG_TS, AV1)

    private var isPlaying = false

    fun play() {
...
    }
}

Cela seul semble assez simple à utiliser.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val videoPlayer = VideoPlayer()
        videoPlayer.play()
    }
}

Cependant, avec cela seul, les problèmes suivants se produiront pendant le processus de développement.

** Essayez une simple DI pour contourner facilement ce problème! ** **

Let's DI facilement

Voici l'injection de dépendance (DI). En japonais, il s'agit d'une injection de dépendances. Dans un premier temps, je vais vous montrer brièvement comment injecter des dépendances en utilisant un constructeur appelé ** Constructor Injection **. Passez simplement la dépendance au ** constructeur ** de VideoPlayer. Une autre méthode d'injection avec un setter est appelée injection setter.

Exemple d'injection de constructeur Différence: https://github.com/takahirom/hilt-sample-app/commit/a1fdef28515d158577313b90f7c2590bd5905366

** VideoPlayer est simple avec des dépendances interchangeables! ** **

class VideoPlayer(
    private val database: VideoDatabase,
    private val codecs: List<Codec>
) {
    private var isPlaying = false

    fun play() {
...
    }
}

Si vous utilisez l'injection de constructeur, vous devez d'abord créer une dépendance pour cette classe de votre côté pour l'instancier. ** **

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val videoDatabase = Room
            .databaseBuilder(
                this,
                VideoDatabase::class.java,
                "database"
            )
            .createFromAsset("videos.db")
            .build()
        val codecs = listOf(FMP4, WebM, MPEG_TS, AV1)
        val videoPlayer = VideoPlayer(videoDatabase, codecs)
        videoPlayer.play()
    }
}

Cela peut être une ** plaque chauffante **, car vous devrez écrire un code comme celui-ci chaque fois que vous créez un VideoPlayer sur différents écrans. Si vous regardez attentivement ce processus, vous pouvez voir qu'il existe ** "processus de création de VideoPlayer" et "processus d'appel de VideoPlayer # play". ** **

Traitement pour créer VideoPlayer

"Traitement pour créer VideoPlayer" est juste la logique de création d'autres types et classes, qui s'appelle "** logique de construction **".

val videoDatabase = Room
    .databaseBuilder(
        this,
        VideoDatabase::class.java,
        "database"
     )
     .createFromAsset("videos.db")
     .build()
val codecs = listOf(FMP4, WebM, MPEG_TS, AV1)
val videoPlayer = VideoPlayer(videoDatabase, codecs)

Processus pour appeler VideoPlayer # play

Le "processus qui appelle VideoPlayer # play" devient la logique de création de la valeur de l'application et s'appelle "** logique métier **". Appelons ici autre chose que la logique de construction.

videoPlayer.play()

La combinaison de la logique de construction et de la logique métier rend difficile le suivi et la lecture du code. De plus, cela n'a souvent pas beaucoup de sens pour le lecteur de la classe. ** Les bibliothèques d'injection de dépendances (DI) peuvent séparer cette logique de construction de la logique métier. ** **

Résumé sur DI

DI est difficile sur Android

Le framework crée des instances telles que Activity. ** Par exemple, startActivity créera une instance **. (Vous ne pouvez pas jouer avec le constructeur.)

//L'activité est instanciée par le framework Android
startActivity(Intent(context, MainActivity::class))

Des améliorations ont été apportées, telles que la possibilité de créer des activités avec Factory à partir d'Android API niveau 28, mais ce n'est pas réaliste pour le moment car cela ne fonctionnera pas à moins qu'il s'agisse d'Android 9 ou supérieur.

Dagger a été la solution jusqu'à présent.

** 74% des 10 000 meilleures applications utilisent Dagger **, qui est actuellement la principale solution. Cependant, selon l'enquête, ** 49% des utilisateurs avaient besoin d'une meilleure solution DI **.

De quelle solution DI aviez-vous besoin?

Cela semble être la principale raison pour laquelle Dagger Hilt est né.

Dagger Hilt Dagger Hilt est une bibliothèque construite sur Dagger. Vous pouvez utiliser les bons points de Dagger. Il est co-créé par les équipes Android X et Dagger de Google.

Utilisons Dagger Hilt dans l'exemple du lecteur vidéo

Je dois apprendre à Dagger Hilt comment créer un VideoPlayer, comment puis-je l'enseigner? La façon la plus simple d'apprendre à créer une instance dans ** Dagger Hilt est d'ajouter @ Inject au constructeur **. Actuellement, VideoPlayer n'a aucune dépendance. ** Dagger Hilt peut être créé car il instancie simplement cette classe. ** **

class VideoPlayer @Inject constructor() {
    private var isPlaying = false

    fun play() {
...
    }
}

Ensuite, nous devons dire à Hilt que cette application fonctionne avec Hilt. L'utilisation de @ HiltAndroidApp dans la classe Application Android vous indiquera que cette application fonctionne avec Hilt. De plus, si vous utilisez ** @ HiltAndroidApp, Component sera créé en interne **. Ce ** composant est la partie qui contient la logique de construction et l'instance créée **. (Je reparlerai des composants plus tard.)

@HiltAndroidApp
class VideoApp : Application()

Il a été dit que le constructeur Activty ne pouvait pas être falsifié, mais en réponse, ajoutez @ AndroidEntryPoint à Activity.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

En ajoutant @ AndroidEntryPoint, nous enseignons trois choses à Hilt.

La variable est annotée avec «@ Inject». Cela signifie qu'il sera injecté à partir de Hilt. ** C'est un formulaire qui enseigne à Hilt "Inject VideoPlayer when Actiivty is created" **. Et vous pouvez faire tout ce que vous voulez avec la méthode onCreate, comme appeler VideoPlayer.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    //↓ Injecté par onCreate par Dagger Hilt
    @Inject lateinit var videoPlayer: VideoPlayer
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        videoPlayer.play()
    }
}

Différence: https://github.com/takahirom/hilt-sample-app/commit/6a8a3711808e806e5953712adeb19b11cb73c3a9#diff-bbc9d28d8bcbd080a704cacc92b8cf37R24

la magie? Où sont-ils affectés au terrain?

Je pense que c'est plus facile à comprendre si vous connaissez un peu le mécanisme du contenu, alors je vais vous l'expliquer. Les activités avec @ AndroidEntryPoint sont converties par Hilt. Entre MainActivity et AppCompatActivity après conversion par Hilt Contient le ** généré Hilt_MainActivity **. ** Injecté dans le champ dans onCreate de Hilt_MainActivity. ** **

image.png

Code converti par Hilt

@AndroidEntryPoint
class MainActivity : Hilt_MainActivity() { // Hilt_C'est l'activité principale
    @Inject lateinit var videoPlayer: VideoPlayer
    override fun onCreate(savedInstanceState: Bundle?) {
        //Injecté sur le terrain dans ce
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        videoPlayer.play()
    }
}


Comment faire en sorte que VideoPlayer injecte VideoDatabase?

Vous pouvez maintenant utiliser le lecteur vidéo dans l'activité principale. Cependant, cela ne permet pas au lecteur vidéo d'accéder à la base de données, etc. Que devrais-je faire?

//↓ Je n'ai pas de base de données, etc.
class VideoPlayer @Inject constructor() {
    private var isPlaying = false

    fun play() {
...
    }
}

Si vous pouvez changer le constructeur de ** VideoDatabase, vous pouvez également ajouter @ Inject à VideoDatabase et Hilt vous apprendra comment créer une instance. Dagger créera une instance de dépendance, VideoDatabase, puis instanciera VideoPlayer. ** **

class VideoPlayer @Inject constructor(
  private val database: VideoDatabase
) {
    private var isPlaying = false

    fun play() {
...
    }
}
class VideoDatabase @Inject constructor() {
...
}

Cependant, cette fois, ce sera une instance créée par Room, vous ne pouvez donc pas ajouter @ Inject en jouant avec le constructeur. Je ne me dis pas et ne me dis pas comment créer la base de données vidéo suivante sur Dagger Hilt pour cela.

val videoDatabase = Room
    .databaseBuilder(
        this,
        VideoDatabase::class.java,
        "database"
     )
     .createFromAsset("videos.db")
     .build()

Nous utilisons donc Module. ** Vous pouvez apprendre à Hilt à créer une instance à l'aide de Module. ** ** C'est juste une classe avec les annotations @ Module et @ InstallIn. Ajoutez une méthode à ce module. ** Il est facile de considérer les méthodes du module comme des recettes de cuisine. C'est une recette pour fabriquer des moules pour VideoDatabase, et j'enseigne cette recette à Hilt. ** ** ** Placez cette recette dans SingletonComponent avec @ InstallIn. SingletonComponent signifie ajouter au composant de l'application. ** **

Si vous regardez attentivement la méthode, vous pouvez voir qu'elle dit «@ Provides». Il enseigne à Hilt une méthode qui enseigne comment créer une base de données vidéo. Lorsque Hilt doit créer une VideoDatabase, il exécute cette méthode et renvoie une instance. À propos, une instance de la classe de contexte est fournie par @ ApplicationContext, mais il existe également des classes prédéfinies par Hilt, et Hilt fournit des instances. Différence: https://github.com/takahirom/hilt-sample-app/commit/c85a6f668a0bf447c0a4b119f4f6d8cc8c2cff80

@Module
@InstallIn(SingletonComponent::class)
object DataModule {
    @Provides
    fun provideVideoDB(@ApplicationContext context: Context): VideoDatabase {
        return Room
            .databaseBuilder(
                context,
                VideoDatabase::class.java,
                "database"
            )
            .createFromAsset("videos.db")
            .build()
    }
}

Maintenant que nous avons ce SingletonComponent, parlons un peu plus de Component.

Component

Le composant peut:

Composant standard Dagger Hilt

Hilt a dit qu'il était Opnionné, mais Dagger Hilt a un ** composant standard, vous n'avez donc pas à vous soucier de la structure du composant. ** ** Cette figure montre la hiérarchie des composants et a une structure avec des composants tels que SingletonComponent de l'application entière, ActivityRetainedCompoent qui survit à la rotation de l'écran et ActivityComponent lié à Activity. Les annotations ci-dessus sont des annotations de portée. J'en parlerai plus tard Par exemple, il est livré avec ActivityRetainedComponent, ActivityComponent et FragmentComponent qui survivent à SingletonComponent et à la modification de configuration.

image.png Sur https://dagger.dev/hilt/components

Dans cet exemple, Singleton Component explique comment créer VideoPlayer et VideoDatabase, ainsi que l'ordre de création. image.png

Que faire si vous souhaitez partager une instance

Par exemple, actuellement, la base de données vidéo est instanciée chaque fois qu'elle est utilisée.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var videoPlayer: VideoPlayer
    @Inject
    lateinit var videoPlayer2: VideoPlayer
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        println(videoPlayer.database)
        // VideoDatabase_Impl@764b474 ← Le code de hachage est différent
        println(videoPlayer2.database)
        // VideoDatabase_Impl@a945d9d ← Le code de hachage est différent
    }
}

class VideoPlayer @Inject constructor(
    val database: VideoDatabase
)
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
    @Provides
    fun provideVideoDB(@ApplicationContext context: Context): VideoDatabase {
        return ...
    }
}

Vous souhaiterez peut-être recycler les instances pour diverses raisons, telles que la réutilisation des connexions. De plus, bien que cela ne soit pas lié à cet exemple, OkHttp, qui est couramment utilisé dans la communication Android, améliorera les performances si l'instance est utilisée dans toute l'application. (De https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/)

Si vous souhaitez le partager dans Activity, il sera conservé dans ActivityComponent en utilisant ** @ ActivityScoped **, donc Vous pouvez réutiliser la même instance dans l'activité. Ce @ ActivityScoped s'appelle l'annotation de portée.

Vous pouvez maintenant partager l'instance au sein de l'activité. Différence: https://github.com/takahirom/hilt-sample-app/commit/f895dfac123a0317b9e0e247af3a48b57388ad5d

@Module
@InstallIn(ActivityComponent::class)
object DataModule {
    @ActivityScoped
    @Provides
    fun provideVideoDB(@ApplicationContext context: Context): VideoDatabase {
...
    }
}

Dans @ ActivityScoped, si l'activité est différente, ce sera une instance différente. Si vous souhaitez l'utiliser pour l'ensemble de l'application, vous pouvez l'utiliser pour l'ensemble de l'application, car il est conservé par Singleton Component en utilisant ** @ Singleton Scope Annotation **. Différence: https://github.com/takahirom/hilt-sample-app/commit/64cc1b50388cf9c79fba26a774ae86efb1f093bc

@Module
@InstallIn(SingletonComponent::class)
object DataModule {
    @Singleton
    @Provides
    fun provideVideoDB(@ApplicationContext context: Context): VideoDatabase {
...
    }
}

VideoDtabase contient désormais également des instances dans SingletonComponent.

image.png

Lorsque vous souhaitez utiliser les dépendances Hilt d'une classe que Hilt ne gère pas

Les dépendances peuvent être obtenues auprès de Hilt dans les classes MainActivity et VideoPlayer gérées par Hilt, mais il peut être difficile de les obtenir dans des classes non gérées par Hilt. Par exemple, la classe ContentProvider, les classes générées par d'autres bibliothèques, les classes existantes lors de la migration vers Dagger Hilt, etc. Ici, vous pouvez utiliser un mécanisme appelé EntryPoint. ** En utilisant EntryPoint, vous pouvez accéder aux dépendances du composant Hilt. ** **

Voici un exemple de Hilt utilisant VideoPlayer avec une logique de construction dans une activité qui ne peut pas être gérée par Hilt. Différence: https://github.com/takahirom/hilt-sample-app/commit/d66fb46b395b0c9b6a98ff91bd55f3c4f12c99c9

class NonHiltActivity : AppCompatActivity() {
    @EntryPoint // @Ajouter un point d'entrée.
    @InstallIn(SingletonComponent::class)
    interface NonHiltActivityEntryPoint {
        fun videoPlayer(): VideoPlayer
    }

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        val entryPoint = EntryPointAccessors.fromApplication(
            applicationContext,
            NonHiltActivityEntryPoint::class.java
        )
        val videoPlayer = entryPoint.videoPlayer()
        videoPlayer.play()
    }
}

tester

Vous pouvez écrire le test en créant vous-même l'objet cible nouveau de la même manière que pour écrire un test général. Dans ce cas, vous devez d'abord créer la dépendance vous-même. Différence: https://github.com/takahirom/hilt-sample-app/commit/068082bf7bcb20ecbb1258ac6a3027988d624303

    @Test
    fun normalTest() {
        //Créer un DB en mémoire
        val database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            VideoDatabase::class.java
        ).build()
        val videoPlayer = VideoPlayer(database)

        videoPlayer.play()

        assertThat(videoPlayer.isPlaying, `is`(true))
    }

Hilt vous permet de laisser Hilt créer une instance comme celle-ci ** sans avoir à créer vos propres dépendances. ** ** Mais cette fois, je veux l'utiliser avec la base de données en mémoire au lieu d'utiliser la base de données réelle. Comment feriez-vous cela?

@HiltAndroidTest
class VideoPlayerTest {
    @get:Rule
    var hiltAndroidRule = HiltAndroidRule(this)

    @Inject
    lateinit var videoPlayer: VideoPlayer

    @Test
    fun play() {
        hiltAndroidRule.inject()

        videoPlayer.play()

        assertThat(videoPlayer.isPlaying, `is`(true))
    }

En ** @UninstallModules (DataModule :: class) ici, vous pouvez faire en sorte que Dagger Hilt oublie comment créer une base de données vidéo que DataModule a **. Ensuite, dans le test **, vous pouvez fournir la base de données en définissant un nouveau module. ** Si vous déclarez Module en dehors du test, il sera installé tout au long du test. Différence: https://github.com/takahirom/hilt-sample-app/commit/4c862ee62e8dfc133ea6e7e3ff0735c0497cfb6a

@HiltAndroidTest
@UninstallModules(DataModule::class)
@RunWith(RobolectricTestRunner::class)
class VideoPlayerTest {
    @InstallIn(SingletonComponent::class)
    @Module
    class TestDataModule {
        @Provides
        fun provideVideoDatabase(): VideoDatabase {
            return Room.inMemoryDatabaseBuilder(
                ApplicationProvider.getApplicationContext(),
                VideoDatabase::class.java
            ).build()
        }
    }

À propos de la migration de Dagger Hilt depuis Dagger

C'est une histoire pour les gens qui connaissent Dagger, donc si vous ne comprenez pas Dagger, regardez-la.

Je vais aborder étape par étape comment migrer de Dagger à Dagger Hilt.

Préparation à l'introduction à Hilt

Améliorez la bibliothèque Dagger

→ Migrez simplement normalement.

Jetons un coup d'œil au statut des composants de Dagger

Il existe différents plug-ins pour les outils qui produisent des diagrammes de composants Dagger. Dagger SPI (interface du fournisseur de services) à l'intérieur de Dagger Il existe quelque chose comme l'API de Dagger qui peut récupérer des informations, il est donc recommandé de vérifier avec l'outil qui l'utilise.

https://github.com/arunkumar9t2/scabbard https://github.com/Snapchat/dagger-browser Tel

Et dans une certaine mesure, voyons quels composants sont susceptibles d'être associés à quels composants. Exemple de diagramme utilisant un fourreau image.png

Hypothèses de cette situation

L'exemple de situation est qu'il existe un composant AppComponent au niveau de l'application, et qu'il existe de nombreux composants pour chaque activité en dessous. (Étant donné que la forme du composant n'est pas normalisée dans Dagger, la forme du composant sera différente en premier lieu.)

Si vous créez un module en passant un argument dans la méthode de création d'un AppComponent, arrêtez-le

Dagger Hilt ne prend pas en charge la création de ce module ** en le transmettant, vous devez donc vous arrêter. ** C'est possible en supprimant l'argument de Module et en le référençant. Il semble que ce n'est pas bon dans le document de Dagger. (Il est dit de ne pas faire cela https://dagger.dev/dev-guide/testing)

NG

    DaggerAppComponent
      .builder()
      .networkModule(networkModule)
    .build()

OK

    DaggerAppComponent.factory()
      .create(application)

Introduisez Hilt pour remplacer AppComponent

Remplacer le composant au niveau de l'application par le composant Singleton de Dagger Hilt

Présentez la bibliothèque Dagger Hilt. Consultez la documentation de base pour cela. https://dagger.dev/hilt/migration-guide

Si vous souhaitez migrer petit à petit, vous devez installer disableModulesHaveInstallInCheck.

Par défaut avec Dagger Hilt, une erreur se produira lorsqu'il y a un module existant qui ne contient pas @ InstallIn. ** Si vous incluez cette option, vous pouvez simplement inclure la bibliothèque sans obtenir l'erreur. ** **

 javaCompileOptions {
      annotationProcessorOptions {
           // ↓ **Voici+=Notez que s'il n'est pas défini sur, le plug-in Dagger Hilt ajoutera un argument, vous en serez donc accro.**
           arguments += [
               "dagger.hilt.disableModulesHaveInstallInCheck": "true"
           ]
      }
  }

Le ** EntryPoint de Dagger Hilt récupère non seulement les dépendances des composants, mais peut également créer des sous-composants, utilisez donc cette fonctionnalité pour les remplacer **. (Je vais sauter l'explication ici.) Différence: https://github.com/takahirom/hilt-sample-app/commit/8e542f191bb50ce50db30cb2a72a569f7d17b178

@Subcomponent
interface JustDaggerComponent {
    @Subcomponent.Factory
    interface Factory {
        fun create(): JustDaggerComponent
    }

    fun inject(justDaggerActivity: JustDaggerActivity)
}

@InstallIn(SingletonComponent::class)
@EntryPoint
interface JustDaggerEntryPoint {
    fun activityComponentFactory(): JustDaggerComponent.Factory
}

class JustDaggerActivity : AppCompatActivity() {
    @Inject lateinit var videoPlayer: VideoPlayer
    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        // old: appComponent.justDaggerComponent().inject(this)
        val entryPoint = EntryPointAccessors.fromApplication(
            applicationContext,
            JustDaggerEntryPoint::class.java
        )
        entryPoint.activityComponentFactory().create().inject(this)

        videoPlayer.play()
    }
}

Remplacer le composant de l'activité existante par Hilt

En gros, ajoutez @ AndroidEntryPoint et supprimez le traitement Dagger existant. Changez la forme de «JustDaggerActivity» en forme de «MainActivity».

Il y a beaucoup d'autres choses à propos de la migration, mais si vous pensez à codelab, veuillez essayer Codelab.

Coopération entre Dagger Hilt et Jetpack

Les bibliothèques qui fonctionnent avec les composants Jetpack tels que ViewModel et WorkManager, qui sont souvent utilisés dans le développement, sont fournies et peuvent être utilisées. Différence: https://github.com/takahirom/hilt-sample-app/commit/1bec3370fec0fd5b4233db1884e8427bcf91a540 ViewModel est souvent utilisé dans le développement d'applications Android. Regardons d'abord ViewModel. Dans ViewModel, des modifications sont apportées au constructeur, mais c'est généralement difficile car il est créé via Provider, Factory, etc., mais ** Dagger Hilt masque bien cette partie et facilite ViewModel. ** **

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val videoPlayerViewModel: VideoPlayerViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        videoPlayerViewModel.play()
    }
}

class VideoPlayerViewModel @ViewModelInject constructor(
    private val videoPlayer: VideoPlayer
) : ViewModel() {
    fun play() {
        videoPlayer.play()
    }
}

Pratique pratique de poignard

Voir un exemple Google (ou Googler)

Si vous vous perdez, jetez un œil aux exemples d'applications.

Architecture Samples https://github.com/android/architecture-samples/tree/dev-hilt Application Google I / O https://github.com/google/iosched Sunflower https://github.com/android/sunflower chrisbanes/tivi https://github.com/chrisbanes/tivi

Le mode FastInit est activé, vérifiez donc l'impact

En raison de la forme standardisée de Component dans Dagger Hilt, de nombreux composants tels que SingletonComponent auront des liaisons telles que ce type. En général, Dagger prend plus de temps à instancier à mesure que le nombre de cette liaison augmente. Lorsque Dagger Hilt est inséré, le mode fastInit est activé au lieu du mode normal, ce qui permet de gagner du temps. Cependant, il semble y avoir un compromis dans ce processus, alors vérifions-le avec Firebase Performance, Android Vitals, etc. après la sortie **. ** **

    val PROCESSOR_OPTIONS = listOf(
      "dagger.fastInit" to "enabled",

https://github.com/google/dagger/blob/d3c1d2025a87201497aacb0a294f41b322767a09/java/dagger/hilt/android/plugin/src/main/kotlin/dagger/hilt/android/plugin/HiltGradlePlugin.kt#L108

Comparaison du code généré
Si fastInit est désactivé
public final class DaggerApp_HiltComponents_SingletonC extends App_HiltComponents.SingletonC {
  private Provider<Context> provideContextProvider;

  private Provider<VideoDatabase> provideVideoDBProvider;

  private DaggerApp_HiltComponents_SingletonC(
      ApplicationContextModule applicationContextModuleParam) {

    initialize(applicationContextModuleParam);
  }
...

  @SuppressWarnings("unchecked")
  private void initialize(final ApplicationContextModule applicationContextModuleParam) {
    this.provideContextProvider = ApplicationContextModule_ProvideContextFactory.create(applicationContextModuleParam);
    this.provideVideoDBProvider = DoubleCheck.provider(DataModule_ProvideVideoDBFactory.create(provideContextProvider));
  }


  @Override
  public VideoPlayer videoPlayer() {
    return new VideoPlayer(provideVideoDBProvider.get());
  }

fastInit activé Le composant contient maintenant la valeur au lieu du fournisseur qui détient la valeur.

public final class DaggerApp_HiltComponents_SingletonC extends App_HiltComponents.SingletonC {
  private final ApplicationContextModule applicationContextModule;

  private volatile Object videoDatabase = new MemoizedSentinel();

  private DaggerApp_HiltComponents_SingletonC(
      ApplicationContextModule applicationContextModuleParam) {
    this.applicationContextModule = applicationContextModuleParam;
  }

  private VideoDatabase videoDatabase() {
    Object local = videoDatabase;
    if (local instanceof MemoizedSentinel) {
      synchronized (local) {
        local = videoDatabase;
        if (local instanceof MemoizedSentinel) {
          local = DataModule_ProvideVideoDBFactory.provideVideoDB(ApplicationContextModule_ProvideContextFactory.provideContext(applicationContextModule));
          videoDatabase = DoubleCheck.reentrantCheck(videoDatabase, local);
        }
      }
    }
    return (VideoDatabase) local;
  }

  @Override
  public VideoPlayer videoPlayer() {
    return new VideoPlayer(videoDatabase());
  }

Que faire si vous souhaitez transmettre l'ID sur l'écran des détails, etc.?

Étant donné que la structure de Component est normalisée dans Dagger Hilt, il est difficile de créer EpisodeDetailComponent et d'y distribuer l'ID de détail de l'écran. Il existe de nombreuses façons de procéder, mais l'exemple de méthode Google semble en être un. ** Il s'agit d'une méthode de passage direct sans distribution à l'aide de Dagger. ** ** Sur la page officielle de création de composants Hilt, il y a une histoire que le contexte est un peu différent quand on parle de tâches d'arrière-plan, mais "en général, il est plus simple et suffisant de passer par vous-même". Pour Assisted Inject, qui est le plus susceptible d'être transmis, vous pouvez en concevoir un peu lors de la transmission à ViewModel.

https://dagger.dev/hilt/custom-components

for most background tasks, a component really isn’t necessary and only adds complexity where simply passing a couple objects on the call stack is simpler and sufficient.

Passer des arguments dans la pile d'appels est plus simple et suffisant, et Component ne fait qu'ajouter de la complexité. Je dis ça.

Architecture Samples https://github.com/android/architecture-samples/blob/dev-hilt/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragment.kt#L90 Iosched https://github.com/google/iosched/blob/b428d2be4bb96bd423e47cb709c906ce5d02150f/mobile/src/main/java/com/google/samples/apps/iosched/ui/speaker/SpeakerViewModel.kt#L101 Sunflower https://github.com/android/sunflower/blob/2bbe628f3eb697091567c3be8f756cfb7eb7258a/app/src/main/java/com/google/samples/apps/sunflower/PlantDetailFragment.kt#L55 chrisbanes/tivi https://github.com/chrisbanes/tivi/blob/27348c6e4705c707ceaa1edc1a3080efa06109ae/ui-showdetails/src/main/java/app/tivi/showdetails/details/ShowDetailsFragment.kt#L60

Passez la valeur à ViewModel dans le constructeur

Il a été dit que "ID etc. sera passé directement", mais si vous ne pouvez pas le passer avec le constructeur de ViewModel, il sera rendu lateinit ou nullable et ce sera une forme peu dangereuse, non? Une bibliothèque appelée AssistedInject peut être implémentée pour transmettre des arguments de constructeur. (Il semble qu'il sera incorporé dans Dagger dans le futur)

Différence: https://github.com/takahirom/hilt-sample-app/commit/6584808f8fe13cc92317df50d413f828d1dfdf00

Dagger essaie de s'adapter à ce qu'on appelle une injection assistée. Cela signifie que lors de l'injection, le programme peut passer une valeur comme argument. https://github.com/google/dagger/issues/1825

Il existe une bibliothèque qui peut utiliser cela en premier. https://github.com/square/AssistedInject

Plus précisément, vous pouvez transmettre une valeur à ViewModel dans le constructeur à l'aide du contenu essentiel suivant de Googler. https://gist.github.com/manuelvicnt/437668cda3a891d347e134b1de29aee1

Il est difficile de comprendre en substance, donc si vous êtes intéressé par son fonctionnement, veuillez lire ce qui suit. https://qiita.com/takahirom/items/f28ceb7a6d4e69e4dafe

Emplacement de la définition EntryPoint

EntryPoint ne semble fondamentalement pas être utilisé dans l'exemple de Google. Je vais l'introduire car il semble être utilisé pour la migration de grandes applications. EntryPoint peut être écrit n'importe où, mais où dois-je l'écrire? Si vous utilisez EntryPoint pour obtenir la dépendance, vous pouvez réduire le nombre d'objets dépendants en obtenant uniquement la dépendance nécessaire, donc en gros, définissons-la à l'endroit pour l'obtenir et l'utiliser. **.

class NonHiltActivity : AppCompatActivity() {
    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface NonHiltActivityEntryPoint {
        fun videoPlayer(): VideoPlayer
    }

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        val entryPoint = EntryPointAccessors.fromApplication(
            applicationContext,
            NonHiltActivityEntryPoint::class.java
        )
        val videoPlayer = entryPoint.videoPlayer()
        videoPlayer.play()
    }
}

Multi-module

Je ne pense pas qu'il y ait encore de bonne pratique, mais pensons à Dagger Hilt et aux multi-modules. Supposons que vous ayez la configuration de module suivante. image.png

Le module Gradle qui compile la classe Application Tous les modules et constructeurs Hilt Injecter des classes pour l'injection ** Doit être inclus dans les dépendances transitoires **.

Depuis https://developer.android.com/training/dependency-injection/hilt-multi-module?hl=ja

Vous avez donc besoin d'une référence du module racine au module Gradle avec le module Dagger comme suit **. En ce qui concerne cette partie, ** je pense que ça va parce que la forme au milieu se déplace dans divers modèles sans dépendances croissantes inutiles **, mais il n'y a pas encore de meilleure pratique. ** Cependant, lorsque vous créez un module, incluez-le simplement dans le chemin de la classe et le module @ InstallIn sera installé et utilisé dans le composant, il est donc très facile et vraiment facile à utiliser. ** **

image.png

référence Référence du module d'application de l'application chrisbanes / tivi pour Google Vous pouvez voir que chaque module est référencé à partir du module Gradle de la racine. image.png

Pouvez-vous construire avec Hilt? Ne peux pas? N'est-il pas gênant de créer un environnement pour des expériences comme celle-ci? Si vous souhaitez expérimenter avec une application sur laquelle Hilt est installé, nous avons préparé cet exemple de projet, alors veuillez expérimenter. https://github.com/takahirom/dagger-transitive-playground/tree/hilt

Pratique de test

Utilisez de vraies dépendances

Il existe une page appelée Philosophie de test Hilt, qui décrit les pratiques de test avec Dagger Hilt. C'est assez sûr et intéressant, alors lisez-le https://dagger.dev/hilt/testing-philosophy Voici mon mémo. https://qiita.com/takahirom/items/a3e406b067ad645605da

D'après cela, il y a deux choses que je veux dire.

Pourquoi utiliser de vraies dépendances?

Comment utiliser de vraies dépendances avec Dagger Hilt?

Si vous essayez de tester normalement et d'utiliser les dépendances réelles, vous obtiendrez une ** plaque chauffante ** pour créer les dépendances. Si vous écrivez que vous avez besoin d'une ViewModel Factory pour créer un PlayerFragment, vous avez besoin d'un VideoPlayer pour créer un ViewModel, vous avez besoin d'une VideoDatabase pour créer un VideoPlayer, et ainsi de suite. .. ** Ça va être difficile. ** **

launchFragment {
    //Beaucoup!
    PlayerFragment().apply {
        videoPlayerViewModelAssistedFactory =
            object : VideoPlayerViewModel.AssistedFactory {
                override fun create(videoId: String): VideoPlayerViewModel {
                    return VideoPlayerViewModel(
                        videoPlayer = VideoPlayer(
                            database = Room.inMemoryDatabaseBuilder(
                                ApplicationProvider.getApplicationContext(),
                                VideoDatabase::class.java
                            ).build()
                        ),
                        videoId = "video_id"
                    )
                }
            }
    }
}
onView(withText("playing")).check(matches(isDisplayed()))

Comme présenté, vous pouvez également tester avec Dagger Hilt en faisant ** Injecter avec les dépendances réelles. ** **

@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
@UninstallModules(DataModule::class)
class AndroidPlayerFragmentTest {
    @InstallIn(SingletonComponent::class)
    @Module
    class TestDataModule {
        @Provides
        fun provideVideoDatabase(): VideoDatabase {
            return Room.inMemoryDatabaseBuilder(
                ApplicationProvider.getApplicationContext(),
                VideoDatabase::class.java
            ).build()
        }
    }

    @get:Rule
    var hiltAndroidRule = HiltAndroidRule(this)

    @Test
    fun play() {
        hiltAndroidRule.inject()
        launchFragmentInHiltContainer<PlayerFragment> {
        }
        onView(withText("playing")).check(matches(isDisplayed()))
    }

Il y a divers inconvénients, arrêtons donc la classe Application personnalisée dans Test

Pour le moment, vous pouvez tester en utilisant une application personnalisée avec @ CustomTestApplication. Si vous créez une classe TestApp à des fins de test, combinez-la avec Hilt Il existe plusieurs inconvénients. De plus, il semble préférable de l'arrêter en général.

Les fines pointes de Dagger Hilt

Si vous préparez HiltAndroidAutoInjectRule etc., cela fonctionnera sans appeler vous-même Inject.

@get:Rule val hiltAndroidAutoInjectRule = HiltAndroidAutoInjectRule(this)

class HiltAndroidAutoInjectRule(testInstance: Any) : TestRule {
  private val hiltAndroidRule = HiltAndroidRule(testInstance)
  private val delegate = RuleChain
    .outerRule(hiltAndroidRule)
    .around(HiltInjectRule(hiltAndroidRule))

  override fun apply(base: Statement?, description: Description?): Statement {
    return delegate.apply(base, description)
  }
}
class HiltInjectRule(val rule: HiltAndroidRule) : TestWatcher() {
  override fun starting(description: Description?) {
    super.starting(description)
    rule.inject()
  }
}

Résumé

J'ai parlé de:

Vous pouvez vous attendre à divers effets en utilisant Dagger Hilt, alors créons une application en utilisant Dagger Hilt!

référence

Page Web officielle https://dagger.dev/hilt/ https://developer.android.com/training/dependency-injection/hilt-android?hl=ja

Android Dependency Injection https://www.youtube.com/watch?v=B56oV3IHMxg Dagger Hilt Deep Dive https://www.youtube.com/watch?v=4di2TTqeCrE

Architecture Samples https://github.com/android/architecture-samples/tree/dev-hilt Application Google I / O https://github.com/google/iosched Sunflower https://github.com/android/sunflower chrisbanes/tivi https://github.com/chrisbanes/tivi

Recommended Posts

Dagger Hilt (document DevFest 2020)
Pratiques de test basées sur Dagger Hilt