The
@EarlyEntryPoint
annotation provides an escape hatch when a Hilt entry point needs to be created
before the singleton component is available in a Hilt test.
Note that, although
@EarlyEntryPoint
and
EarlyEntryPoints
are mostly used in production code, they only have an effect during Hilt tests.
In production, these entry points behave the same as
@EntryPoint
and
EntryPoints
,
respectively.
Background
In a
Hilt test,
the singleton component’s lifetime is scoped to the lifetime of a test case
rather than the lifetime of the
Application
.
This is useful to prevent leaking state across test cases, but it makes it
impossible to access entry points from a component outside of a test case.
To get a better understanding of why/when this becomes an issue, let’s look at a typical lifecycle of an Android Gradle instrumentation test.
# Typical Application lifecycle during an Android Gradle instrumentation test
- Application created
- Application.onCreate() called
- Test1 created
- SingletonComponent created
- testCase1() called
- Test1 created
- SingletonComponent created
- testCase2() called
...
- Test2 created
- SingletonComponent created
- testCase1() called
- Test2 created
- SingletonComponent created
- testCase2() called
...
- Application destroyed
As the lifecycle above shows, Application#onCreate()
is called before any
SingletonComponent can be created, so calling an entry point from
Application#onCreate()
is not possible. (For the same reason, there are
similar issues with calling entry points from ContentProvider#onCreate()
).
While these cases should be rare, sometimes they are unavoidable. This is where
@EarlyEntryPoint
comes in.
Usage
Annotating an entry point with @EarlyEntryPoint
instead of @EntryPoint
allows the entry point to be called at any point during the lifecyle of a test
application. (Note that an @EarlyEntryPoint
can only be installed in the
SingletonComponent
).
For example:
@EarlyEntryPoint
@InstallIn(SingletonComponent.class)
public interface FooEntryPoint {
Foo foo();
}
@EarlyEntryPoint
@InstallIn(SingletonComponent::class)
interface FooEntryPoint {
fun foo(): Foo
}
Once annotated with @EarlyEntryPoint
, all usages of the entry point must
go through
EarlyEntryPoints#get()
(rather than
EntryPoints#get()
) to
get an instance of the entry point. This requirement makes it clear at the call
site which component will be used during a Hilt test. For example:
// A base application used in a Hilt test that injects objects in onCreate
public abstract class BaseTestApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// Entry points annotated with @EarlyEntryPoint must use
// EarlyEntryPoints rather than EntryPoints.
foo = EarlyEntryPoints.get(this, FooEntryPoint.class).foo();
}
}
// A base application used in a Hilt test that injects objects in onCreate
public abstract class BaseTestApplication: Application {
override fun onCreate() {
super.onCreate()
// Entry points annotated with @EarlyEntryPoint must use
// EarlyEntryPoints rather than EntryPoints.
foo = EarlyEntryPoints.get(this, FooEntryPoint::class).foo()
}
}
Caveats
The component used with EarlyEntryPoints
does not share any state with the
singleton component used for a given test case. Even @Singleton
scoped
bindings will not be shared.
The component used with EarlyEntryPoints
does not have access to any
test-specific bindings (i.e. bindings created within a specific test class such
as @BindValue
or a
nested module).
Finally, the component used with EarlyEntryPoints
lives for the lifetime of
the application, so it can leak state across multiple test cases (e.g. in
Android Gradle instrumentation tests).
When not to use EarlyEntryPoint
Most usages of @EarlyEntryPoint
are needed to allow calling entry points from
within Application#onCreate()
or ContentProvider#onCreate()
. However, before
switching to @EarlyEntryPoint
, try the alternatives listed below.
Entry points for Application getter methods
If the entry point is used to initialize a field that will later be returned in
a getter method, consider removing the field and getter method and replacing it
with a @Singleton
scoped binding that other classes can inject directly rather
than going through the application class.
If the getter method is required (e.g. the application must extend an interface
that requires it to be overriden) then try replacing the field with a
@Singleton
scoped binding and calling EntryPoints.get()
lazily from the
getter method.
Entry points for initialization/configuration
If the entry point is used to perform initialization/configuration (e.g. setting up a logger or prefetching data) then first consider whether this work is necessary for your tests. Most tests, e.g. tests for activities and fragments should not be dependent on this initialization to work properly, since activities and fragments should generally be designed to be reusable in other applications.
If your test needs the initialization/configuration, consider whether it’s okay
to only run the initialization/configuration once and share any state of that
run between tests. If that’s not okay, then you may need to consider moving the
logic into a
TestRule
instead.