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!
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! ** **
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". ** **
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" 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. ** **
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.
** 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.
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
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. ** **
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()
}
}
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:
Has logic on how to build an object. Video Database is created like this. .. Such VideDatabase : Room.databaseBuilder() … .build()
Has the logic of the order of instantiation. "Generate Video Player after Video Database." Etc.
Reuse instances depending on the scope. (See below)
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.
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.
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.
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()
}
}
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()
}
}
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.
→ Just migrate normally.
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
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.)
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 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()
}
}
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.
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()
}
}
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
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
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());
}
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
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 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()
}
}
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.
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. ** **
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.
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
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.
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()))
}
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.
Currently, running the converted code in Android Studio is not supported, so you need to run the test in ** Gradle. ** (You can also set Gradle in Android Studio and run it in Android Studio.)
For more information: https://dagger.dev/hilt/gradle-setup.html#running-with-android-studio
** Since Android Studio 4.1, Run / Debug Configrations can be saved, so you can take advantage of it. ** **
Difference: https://github.com/takahirom/hilt-sample-app/commit/2274ff3b5712e6b266cf022ff91f4581532bf45b
Create rules to ease testing
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()
}
}
I've talked about:
You can expect various effects by using Dagger Hilt, so let's make an app using Dagger Hilt!
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