[JAVA] Dagger Hilt (DevFest 2020 document)

An article version of DevFest material. I will explain each step with the difference of the sample application. We'll start with why you need Dependency Injection, explain Dagger Hilt, and get hands-on practice!

What is Dependency Injection (DI)?

Why you need DI

DI has a slightly difficult image, but why is it necessary in the first place? Let's say you're making a video playback app with a class called VideoPlayer. The database, codec, etc. are ** hard coded in the VideoPlayer class. ** **

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

class VideoPlayer {
    //A list of videos is stored in the database(I'm using a library called Room)
    private val database = Room
        .databaseBuilder(
            App.instance,
            VideoDatabase::class.java,
            "database"
        )
        .createFromAsset("videos.db")
        .build()
    //List of codecs that can be used
    private val codecs = listOf(FMP4, WebM, MPEG_TS, AV1)

    private var isPlaying = false

    fun play() {
...
    }
}

This alone looks simple enough to use.

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

However, with this alone, the following problems will occur during the development process.

** Try a simple DI to easily work around this issue! ** **

Let's DI easily

Here is Dependency Injection (DI). Dependency injection in Japanese. At first, I will briefly show you how to inject dependencies using a constructor called ** Constructor Injection **. Simply pass the dependency to the Video Player's ** constructor **. Another method of injection by setter is called setter injection.

Constructor injection example Difference: https://github.com/takahirom/hilt-sample-app/commit/a1fdef28515d158577313b90f7c2590bd5905366

** VideoPlayer is simple with interchangeable dependencies! ** **

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

    fun play() {
...
    }
}

If you use constructor injection, you must first create a dependency for that class before you can instantiate it. ** **

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

This can be a ** boilerplate ** because you have to write code like this every time you make a VideoPlayer on different screens. If you look closely at this process, you can see that there are ** "process for making VideoPlayer" and "process for calling VideoPlayer # play". ** **

Processing to make a Video Player

The "process for creating a VideoPlayer" is just the logic for creating other types and classes, which is called "** construction logic **".

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)

Process to call VideoPlayer # play

"Process to call VideoPlayer # play" is the logic to create the value of the application and is called "** Business logic **". Let's call something other than construction logic like this here.

videoPlayer.play()

The combination of construction logic and business logic makes it difficult to follow and read the code. It also often doesn't make much sense to the reader of the class. ** Dependency Injection (DI) libraries can separate this construction logic from business logic. ** **

Summary about DI

DI is difficult on Android

The framework creates an instance of Activity etc. ** For example, startActivity will create an instance **. (You can't mess with the constructor.)

//Activity is instantiated by Android framework
startActivity(Intent(context, MainActivity::class))

Improvements have been made, such as being able to create Activities with Factory from Android API Level 28, but it is not realistic at present because it will not work unless it is Android 9 or higher.

Dagger has been the solution so far.

** 74% of the top 10,000 apps use Dagger **, which is the main solution right now. However, according to the survey, ** 49% of users needed a better DI solution **.

What DI solution did you need?

This seems to be the main reason why Dagger Hilt was born.

Dagger Hilt Dagger Hilt is a library built on top of Dagger. You can use the good points of Dagger. It is co-created by Google's Android X team and the Dagger team.

Let's use Dagger Hilt in the Video Player example

I have to teach Dagger Hilt how to make a Video Player somehow, how can I teach it? The most basic way to teach how to create an instance in ** Dagger Hilt is to add @Inject to the constructor **. Currently, VideoPlayer has no dependencies. ** Dagger Hilt just instantiates this class, so you can create it. ** **

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

    fun play() {
...
    }
}

Next you have to tell Hilt that this app works with Hilt. Using @HiltAndroidApp in the Android Application class will tell you that this app works with Hilt. Also, if you use ** @ HiltAndroidApp, Component will be created internally **. This ** Component is the part that holds the construction logic and the created instance **. (I'll talk about Components again later.)

@HiltAndroidApp
class VideoApp : Application()

It was said that the constructor of Activty could not be tampered with, but as a response, add @AndroidEntryPoint to Activity.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

By adding @AndroidEntryPoint, we teach Hilt three things.

The variable is annotated with @Inject. This means that it will be injected from Hilt. ** It is a form that teaches Hilt to "Inject the Video Player when Actiivty is created" **. And you can do whatever you want with the onCreate method, such as calling a VideoPlayer.

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

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

magic? Where are they assigned to the field?

I think it's easier to understand if you know the mechanism of the contents a little, so I will explain it. Activities with @AndroidEntryPoint are converted by Hilt. Between MainActivity and AppCompatActivity after conversion by Hilt Contains the ** generated Hilt_MainActivity **. ** Injected into the field in onCreate of Hilt_MainActivity. ** **

image.png

Code converted by Hilt

@AndroidEntryPoint
class MainActivity : Hilt_MainActivity() { // Hilt_It is the main activity
    @Inject lateinit var videoPlayer: VideoPlayer
    override fun onCreate(savedInstanceState: Bundle?) {
        //Injected into the field in this
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        videoPlayer.play()
    }
}


How do I get a Video Player to inject a Video Database?

Now you can use Video Player in Main Activity. However, with this, Video Player cannot access Database etc. What should I do?

//↓ I don't have Database etc.
class VideoPlayer @Inject constructor() {
    private var isPlaying = false

    fun play() {
...
    }
}

If you can change the constructor of ** VideoDatabase, you can also add @Inject to VideoDatabase and Hilt will teach you how to create an instance. Dagger will instantiate the dependency instance, VideoDatabase, and then VideoPlayer. ** **

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

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

However, this time it will be an instance created by Room, so you can not add @Inject by playing with the constructor. I do not and do not tell me how to make the following VideoDatabase to Dagger Hilt somehow for that.

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

So we use Module. ** Module can be used to teach Hilt how to create an instance. ** ** It's just a class that annotates @Module and @InstallIn. Add a method to that Module. ** It's easy to think of the methods in Module as cooking recipes. It's a recipe for making molds for VideoDatabase, and I'm teaching Hilt this recipe. ** ** ** Place this recipe in SingletonComponent with @ InstallIn. SingletonComponent means to add to the Component of the Application. ** **

If you look closely at the method, you can see that it says @ Provides. It teaches Hilt a method that teaches how to create a Video Database. When Hilt needs to create a VideoDatabase, it executes this method and returns an instance. By the way, an instance of the context class is provided by @ApplicationContext, but there are also some classes predefined by Hilt, and Hilt provides some instances. Difference: 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()
    }
}

Now that we have this SingletonComponent, let's touch on Component a little more.

Component

The Component can:

Dagger Hilt Standard Component

Hilt said it was Opnioned, but Dagger Hilt has a ** standard Component so you don't have to worry about the structure of the Component. ** ** This figure shows the hierarchy of Components, and has a structure with Components such as SingletonComponent of the entire application, ActivityRetainedCompoent that survives screen rotation, and ActivityComponent linked to Activity. The annotation attached above is a scope annotation. I'll talk about this later For example, it comes with ActivityRetainedComponent, ActivityComponent, and FragmentComponent that survive SingletonComponent and Configuration Change.

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

In this example, Singleton Component contains how to make a Video Player and how to make a Video Database, and also the order of making them. image.png

What to do if you want to share an instance

For example, currently Video Database is instantiated every time it is used.

@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 ← Hash code is different
        println(videoPlayer2.database)
        // VideoDatabase_Impl@a945d9d ← Hash code is different
    }
}

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

You may want to recycle instances for a variety of reasons, such as reusing connections. Also, although it is not related to this example, OkHttp, which is commonly used in Android communication, will improve performance if the instance is used throughout the application. (From https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/)

If you want to share it in Activity, it will be retained in ActivityComponent by using ** @ ActivityScoped **, so You can reuse the same instance in the Activity. This @ActivityScoped is called Scope Annotation.

You can now share the instance within Activity. Difference: https://github.com/takahirom/hilt-sample-app/commit/f895dfac123a0317b9e0e247af3a48b57388ad5d

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

In @ActivityScoped, if the activity is different, it will be a different instance. If you want to use it for the whole app, you can use it for the whole app because it is retained by Singleton Component by using Scope Annotation of ** @ Singleton **. Difference: https://github.com/takahirom/hilt-sample-app/commit/64cc1b50388cf9c79fba26a774ae86efb1f093bc

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

VideoDtabase now also holds instances within SingletonComponent.

image.png

When you want to use Hilt dependencies from a class that Hilt does not manage

Dependencies can be obtained from Hilt in the MainActivity and VideoPlayer classes managed by Hilt, but it may be difficult to obtain them in classes not managed by Hilt. For example, the ContentProvider class, classes generated by other libraries, existing classes when migrating to Dagger Hilt, and so on. Here, you can use a mechanism called EntryPoint. ** By using EntryPoint, you can access the dependencies of Hilt's Component. ** **

This is an example of Hilt using a Video Player with construction logic in an Activity that cannot be managed by Hilt. Difference: https://github.com/takahirom/hilt-sample-app/commit/d66fb46b395b0c9b6a98ff91bd55f3c4f12c99c9

class NonHiltActivity : AppCompatActivity() {
    @EntryPoint // @Add Entry Point.
    @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()
    }
}

test

You can write the test by creating the target object new by yourself in the same way as writing a general test. In this case, you have to create the dependency yourself first. Difference: https://github.com/takahirom/hilt-sample-app/commit/068082bf7bcb20ecbb1258ac6a3027988d624303

    @Test
    fun normalTest() {
        //Create a DB in memory
        val database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            VideoDatabase::class.java
        ).build()
        val videoPlayer = VideoPlayer(database)

        videoPlayer.play()

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

Hilt allows you to instantiate Hilt in this way ** without having to create your own dependencies. ** ** But this time I want to use it with Database in memory instead of using the actual Database. How would you do it?

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

By ** @UninstallModules (DataModule :: class) here, you can make Dagger Hilt forget how to create a Video Database that DataModule has **. Then, in the ** test, you can provide the Database by defining a new Module. ** If you declare Module outside the test, it will be installed throughout the test. Difference: 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()
        }
    }

About Dagger Hilt migration from Dagger

It's a story for people who know Dagger, so if you don't understand Dagger, just look at it.

I will touch on how to migrate from Dagger to Dagger Hilt step by step.

Preparation for introduction to Hilt

Upgrade the Dagger library

→ Just migrate normally.

Let's take a look at the status of Dagger components

There are various plug-ins for tools that output diagrams of Dagger components for a long time. Inside Dagger called Dagger SPI (Service provider interface) There is something like Dagger's API that can retrieve information, so it is recommended to check with the tool that uses it.

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

And to some extent, let's see which components are likely to be associated with which components. Example of diagram using scabbard image.png

Assumptions for this situation

In the example situation, let's assume that there is an Application level component AppComponent, and there are many Components for each Activity under it. (Since the shape of the component is not standardized with Dagger, the shape of the component will be different in the first place.)

If you are creating a Module by passing an argument in the method of creating an AppComponent, stop it

Dagger Hilt doesn't support how to make this ** module by passing it, so you need to stop. ** It is possible by removing the argument of Module and referencing it. It seems that it is not good in the document of Dagger. (It says don't do this https://dagger.dev/dev-guide/testing)

NG

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

OK

    DaggerAppComponent.factory()
      .create(application)

Introduce Hilt to replace AppComponent

Replace Application level Component with Dagger Hilt's Singleton Component

Introduce the Dagger Hilt library. Check the basic documentation for this. https://dagger.dev/hilt/migration-guide

If you want to migrate little by little, you need to install disableModulesHaveInstallInCheck.

By default with Dagger Hilt, an error will occur when there is an existing module that does not contain @InstallIn. ** If you include this option, you can just include the library without getting the error. ** **

 javaCompileOptions {
      annotationProcessorOptions {
           // ↓ **Here is+=Note that if it is not set to, the Dagger Hilt plugin will add an argument, so you will be addicted to it.**
           arguments += [
               "dagger.hilt.disableModulesHaveInstallInCheck": "true"
           ]
      }
  }

Dagger Hilt's ** EntryPoint not only fetches dependencies from Components, but also allows you to create subcomponents, so use that feature to replace them **. (I will skip the explanation here.) Difference: 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()
    }
}

Replace Component of existing Activity with Hilt

Basically, add @AndroidEntryPoint and remove the existing Dagger processing. Change the shape of JustDaggerActivity to the shape of MainActivity.

There are many other things about migration, but if you are thinking about codelab, please try Codelab.

Cooperation between Dagger Hilt and Jetpack

Libraries that work with Jetpack Components such as ViewModel and WorkManager, which are often used in development, are provided and can be used. Difference: https://github.com/takahirom/hilt-sample-app/commit/1bec3370fec0fd5b4233db1884e8427bcf91a540 ViewModel is often used in Android application development. Let's first look at the ViewModel. In ViewModel, changes are made to the constructor, but it is usually difficult because it is created via Provider, Factory, etc., but ** Dagger Hilt hides this part well and makes ViewModel easily. ** **

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

Dagger Hilt Practical Practice

See Google (or Googler) sample

If you get lost, take a look at the sample apps.

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

FastInit mode is enabled, so check the impact

Due to the standardized form of Component in Dagger Hilt, many components such as SingletonComponent will have bindings such as this type. In general, Dagger will take longer to instantiate as the number of this binding increases. When Dagger Hilt is turned on, fastInit mode is enabled instead of normal mode, which saves time. However, there seems to be a trade-off in this process, so let's check it with Firebase Performance, Android Vitals, etc. after ** release. ** **

    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

Comparison of generated code
If fastInit is disabled
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 enabled The Component now holds the value instead of the Provider holding the value.

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

What if you want to pass the ID on the details screen etc.?

Since the structure of Component is standardized in Dagger Hilt, it is difficult to create EpisodeDetailComponent and distribute the screen detail ID there. There are many possible ways to do this, but the Google sample method seems to be one. ** This is a method of passing directly without distributing using Dagger. ** ** On the official page for creating Hilt Components, there is a story about background tasks, but the context is a little different, but "usually it's simpler and more than you pass it yourself". For Assisted Inject, which is most likely to be passed, you can just devise a little when passing it to 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.

Passing arguments in the call stack is simpler and sufficient, and Component only adds complexity. I'm saying that.

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

Pass the value to the ViewModel in the constructor

It was said that "ID etc. will be passed directly", but if you can not pass it in the constructor of ViewModel, it will be lateinit or nullable and it will be a little unsafe form, right? A library called AssistedInject can be implemented to pass constructor arguments. (It seems that it will be incorporated in Dagger in the future)

Difference: https://github.com/takahirom/hilt-sample-app/commit/6584808f8fe13cc92317df50d413f828d1dfdf00

Dagger is trying to accommodate what is called Assisted Inject. This means that when injecting, the program can pass a value as an argument. https://github.com/google/dagger/issues/1825

There is a library that can use this first. https://github.com/square/AssistedInject

Specifically, you can pass the value to ViewModel in the constructor using the following gist contents of Googler. https://gist.github.com/manuelvicnt/437668cda3a891d347e134b1de29aee1

It is difficult to understand in essence, so if you are interested in how it works, please read the following. https://qiita.com/takahirom/items/f28ceb7a6d4e69e4dafe

EntryPoint definition location

EntryPoint basically doesn't seem to be used in Google's sample. I will introduce it because it seems to be used for migration of large applications. You can write the EntryPoint anywhere, but where should you write it? If you use EntryPoint to get the dependency, you can reduce the number of dependent objects by getting only the necessary dependency, so basically, let's define it in the place to get it and use it. **.

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

I don't think there are good best practices yet, but let's think about Dagger Hilt and multi-modules. Suppose you have the following module configuration. image.png

The Gradle module that compiles the Application class All Hilt Modules and Constructors Inject classes for injection ** Must be included in transitive dependencies **.

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

So you need a reference from the root module to the Gradle module with the Dagger Module as follows **. Regarding this part, ** I think it's okay because the shape in the middle moves in various patterns without unnecessarily increasing dependencies **, but there is no best practice yet. ** However, it is very easy and really easy to use because the Module that was @InstallIn is installed in the component and can be used just by including it in the classpath when creating the module. ** **

image.png

reference Reference from the app module of the chrisbanes / tivi app for Google You can see that each module is referenced from the root Gradle module. image.png

Can you build with Hilt? Can not? Isn't it troublesome to create an experimental environment like this? If you want to experiment with an app that has Hilt installed, we have prepared this sample project, so please experiment. https://github.com/takahirom/dagger-transitive-playground/tree/hilt

Testing practice

Use real dependencies

There is a page called Hilt Testing Philosophy, which describes the practices for testing with Dagger Hilt. This is quite assertive and interesting, so please read it. https://dagger.dev/hilt/testing-philosophy Here is my note. https://qiita.com/takahirom/items/a3e406b067ad645605da

According to this, there are two things I want to say.

Why use real dependencies?

How to use real dependencies with Dagger Hilt?

If you try to test it normally and use the actual dependency, you will get a ** boilerplate ** to create the dependency **. If you write that you need a ViewModel Factory to create a PlayerFragment, you need a VideoPlayer to create a ViewModel, you need a VideoDatabase to create a VideoPlayer, and so on. .. ** It will be difficult. ** **

launchFragment {
    //Very much!
    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()))

As introduced, you can also test with Dagger Hilt by doing ** Inject with the actual dependencies. ** **

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

There are various disadvantages, so let's stop the Custom Application class in Test

You can test using a custom application with @CustomTestApplication. If you are making a TestApp class for testing, combine it with Hilt There are various disadvantages. Also, it seems better to stop it in general.

Dagger Hilt's fine tips

If you prepare HiltAndroidAutoInjectRule etc., it will work without calling Inject yourself.

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

Summary

I've talked about:

You can expect various effects by using Dagger Hilt, so let's make an app using Dagger Hilt!

reference

Official web page 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 Google I / O app https://github.com/google/iosched Sunflower https://github.com/android/sunflower chrisbanes/tivi https://github.com/chrisbanes/tivi

Recommended Posts

Dagger Hilt (DevFest 2020 document)
Testing practices based on Dagger Hilt