I want to use the Dagger Provider<T>
interface in Hilt for async initialization as described in section Deferring Dagger Init using Provider of this article. Until now I've only worked with Hilt. How can I use this Dagger feature with Hilt? My goal is to async (non main thread) initialize a class, that will then be provided as singleton.
CodePudding user response:
I think this example have exactly what you want. I will include some code snippets to make this a complete answer.
private static final class ViewModelCImpl extends MyAwesomeApplication_HiltComponents.ViewModelC {
private final SavedStateHandle savedStateHandle;
private final DaggerMyAwesomeApplication_HiltComponents_SingletonC singletonC;
private final ActivityRetainedCImpl activityRetainedCImpl;
private final ViewModelCImpl viewModelCImpl = this;
private Provider<ExampleViewModel> exampleViewModelProvider;
private ViewModelCImpl(DaggerMyAwesomeApplication_HiltComponents_SingletonC singletonC,
ActivityRetainedCImpl activityRetainedCImpl, SavedStateHandle savedStateHandleParam) {
this.singletonC = singletonC;
this.activityRetainedCImpl = activityRetainedCImpl;
this.savedStateHandle = savedStateHandleParam;
initialize(savedStateHandleParam);
}
private ExampleViewModel exampleViewModel() {
return new ExampleViewModel(savedStateHandle, new ExampleRepository());
}
@SuppressWarnings("unchecked")
private void initialize(final SavedStateHandle savedStateHandleParam) {
this.exampleViewModelProvider = new SwitchingProvider<>(singletonC, activityRetainedCImpl, viewModelCImpl, 0);
}
@Override
public Map<String, Provider<ViewModel>> getHiltViewModelMap() {
return Collections.<String, Provider<ViewModel>>singletonMap("com.ddmeng.howhiltworks.ExampleViewModel", (Provider) exampleViewModelProvider);
}
private static final class SwitchingProvider<T> implements Provider<T> {
private final DaggerMyAwesomeApplication_HiltComponents_SingletonC singletonC;
private final ActivityRetainedCImpl activityRetainedCImpl;
private final ViewModelCImpl viewModelCImpl;
private final int id;
SwitchingProvider(DaggerMyAwesomeApplication_HiltComponents_SingletonC singletonC,
ActivityRetainedCImpl activityRetainedCImpl, ViewModelCImpl viewModelCImpl, int id) {
this.singletonC = singletonC;
this.activityRetainedCImpl = activityRetainedCImpl;
this.viewModelCImpl = viewModelCImpl;
this.id = id;
}
@SuppressWarnings("unchecked")
@Override
public T get() {
switch (id) {
case 0: // com.ddmeng.howhiltworks.ExampleViewModel
return (T) viewModelCImpl.exampleViewModel();
default: throw new AssertionError(id);
}
}
}
}
Notice how SwitchingProvider
class implements Provider<T>
as singleton.
CodePudding user response:
Hilt has predefined components for Android that are managed for you. However, there may be situations where the standard Hilt components do not match the object lifetimes or needs of a particular feature. In these cases, you may want a custom component. However, before creating a custom component, consider if you really need one as not every place where you can logically add a custom component deserves one.
For example, consider a background task. The task has a reasonably well-defined lifetime that could make sense for a scope. Also, if there were a request object for that task, binding that into Dagger may save some work passing that around as a parameter. However, 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. Before commiting to adding a custom component, consider the following drawbacks.
Adding a custom component has the following drawbacks:
Each component/scope adds cognitive overhead.
They can complicate the graph with combinatorics (e.g. if the component is a child of the ViewComponent conceptually, two components likely need to be added for ViewComponent and ViewWithFragmentComponent).
Components can have only one parent. The component hierarchy can’t form a diamond. Creating more components increases the likelihood of getting into a situation where a diamond dependency is needed. Unfortunately, there is no good solution to this diamond problem and it can be difficult to predict and avoid.
Custom components work against standardization. The more custom components are used, the harder it is for shared libraries. With those in mind, these are some criteria you should use for deciding if a custom component is needed:
The component has a well-defined lifetime associated with it.
The concept of the component is well-understood and widely applicable. Hilt components are global to the app so the concepts should be applicable everywhere. Being globally understood also combats some of the issues with cognitive overhead.
Consider if a non-Hilt (regular Dagger) component is sufficient. For components with a limited purpose sometimes it is better to use a non-Hilt component.For example, consider a production component that represents a single background task. Hilt components excel in situations where code needs to be contributed from possibly disjoint/modular code. If your component isn’t really meant to be extensible, it may not be a good match for a Hilt custom component.
Custom component limitations
Custom component definitions currently have some limitations:
Components must be a direct or indirect child of the SingletonComponent. Components may not be inserted between any of the standard components. For example, a component cannot be added between the ActivityComponent and the FragmentComponent.
Adding a custom Hilt component
To create a custom Hilt component, create a class annotated with @DefineComponent. This will be the class used in @InstallIn annotations.
The parent of your component should be defined in the value of the @DefineComponent annotation. Your @DefineComponent class can also be annotated with a scope annotation to allow scoping objects to this component.
@DefineComponent(parent = SingletonComponent::class)
interface MyCustomComponent
A builder interface must also be defined. If this builder is missing, the component will not be generated since there will be no way to construct the component. This interface will be injectable from the parent component and will be the interface for creating new instances of your component. As these are custom components, once instances are built, it will be your job to hold on to or release component instances at the appropriate time.
Builder interfaces are defined by marking an interface with @DefineComponent.Builder. Builders must have a method that returns the @DefineComponent type. They may also have additional methods (like @BindsInstance methods) that a normal Dagger component builder may have.
@DefineComponent.Builder
interface MyCustomComponentBuilder {
fun fooSeedData(@BindsInstance foo: Foo): MyCustomComponentBuilder
fun build(): MyCustomComponent
}
While the @DefineComponent.Builder class can be nested within the @DefineComponent, it is usually better as a separate class. It may be separated into a different class as long as it is a transitive dependency of the @HiltAndroidApp application or @HiltAndroidTest test. Since the @DefineComponent class is referenced in many places via @InstallIn, it may be better to separate the builder so that dependencies in the builder do not become transitive dependencies of every module installed in the component.
For the same reason of avoiding excessive dependencies, methods are not allowed on the @DefineComponent interface. Instead, Dagger objects should be accessed via entry points.
@EntryPoint
@InstallIn(MyCustomComponent::class)
interface MyCustomEntryPoint {
fun getBar(): Bar
}
class CustomComponentManager @Inject constructor(
componentBuilder: MyCustomComponentBuilder) {
fun doSomething(foo: Foo) {
val component = componentBuilder.fooSeedData(foo).build();
val bar = EntryPoints.get(component, MyCustomEntryPoint::class.java).getBar()
// Don't forget to hold on to the component instance if you need to!
}