Skip to content

Commit 400e756

Browse files
committed
Add @HiltWorker annotation processor support with @AssistedInject integration
Add support for Hilt injection of WorkManager ListenableWorker classes via the @HiltWorker annotation. This enables Hilt to inject dependencies into WorkManager workers, including support for @AssistedInject constructors. Changes include: - @HiltWorker annotation to mark Worker classes for Hilt injection - HiltWorkerMap qualifier for internal multibinding map - HiltWorkerProcessor (Javac) and KspHiltWorkerProcessor (KSP) - HiltWorkerProcessingStep that processes @HiltWorker annotations - HiltWorkerMetadata validation (checks: extends ListenableWorker, has @Inject/@AssistedInject constructor, non-private, non-scoped) - HiltWorkerModuleGenerator generates: - _HiltModules class with @BINDS and @provides module bindings - _AssistedFactory interface (for @AssistedInject workers) annotated with @AssistedFactory so Dagger generates the factory implementation - HiltWorkerValidationPlugin prevents direct injection of @HiltWorker classes - Tests for @Inject and @AssistedInject worker scenarios Fixes #4490
1 parent 445e6f0 commit 400e756

10 files changed

Lines changed: 824 additions & 0 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (C) 2024 The Dagger Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package dagger.hilt.android.internal.work;
18+
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
import javax.inject.Qualifier;
24+
25+
/**
26+
* Internal qualifier for the multibinding map of @HiltWorker workers.
27+
*/
28+
@Qualifier
29+
@Retention(RetentionPolicy.CLASS)
30+
@Target({ElementType.METHOD, ElementType.PARAMETER})
31+
public @interface HiltWorkerMap {
32+
33+
/** Internal qualifier for the multibinding set of class names annotated with @HiltWorker. */
34+
@Qualifier
35+
@Retention(RetentionPolicy.CLASS)
36+
@Target({ElementType.METHOD, ElementType.PARAMETER})
37+
@interface KeySet {}
38+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (C) 2024 The Dagger Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package dagger.hilt.android.work;
18+
19+
import static java.lang.annotation.ElementType.TYPE;
20+
import static java.lang.annotation.RetentionPolicy.CLASS;
21+
22+
import java.lang.annotation.Documented;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.Target;
25+
26+
/**
27+
* Annotates a class that is a WorkManager {@link
28+
* androidx.work.ListenableWorker} to enable injection with Hilt.
29+
*
30+
* <p>Hilt currently supports the following types of workers:
31+
*
32+
* <ul>
33+
* <li>{@link androidx.work.Worker}
34+
* <li>{@link androidx.work.CoroutineWorker}
35+
* </ul>
36+
*
37+
* <p>The annotated class must have a single constructor annotated with
38+
* {@code @Inject} or {@code @AssistedInject}.
39+
*
40+
* <p>Example usage with a Worker:
41+
* <pre>
42+
* &#64;HiltWorker
43+
* public class MyWorker extends Worker {
44+
* &#64;Inject
45+
* public MyWorker(
46+
* &#64;Assisted Context context,
47+
* &#64;Assisted WorkerParameters workerParams,
48+
* MyDependency myDependency
49+
* ) {
50+
* super(context, workerParams);
51+
* }
52+
*
53+
* &#64;Override
54+
* public Result doWork() { ... }
55+
* }
56+
* </pre>
57+
*/
58+
@Documented
59+
@Retention(CLASS)
60+
@Target(TYPE)
61+
public @interface HiltWorker {}

hilt-compiler/main/java/dagger/hilt/android/processor/internal/AndroidClassNames.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,5 +137,14 @@ public final class AndroidClassNames {
137137
public static final ClassName INJECT_VIA_ON_CONTEXT_AVAILABLE_LISTENER =
138138
get("dagger.hilt.android", "InjectViaOnContextAvailableListener");
139139

140+
// WorkManager-related class names
141+
public static final ClassName LISTENABLE_WORKER = get("androidx.work", "ListenableWorker");
142+
public static final ClassName WORKER_PARAMETERS = get("androidx.work", "WorkerParameters");
143+
public static final ClassName HILT_WORKER = get("dagger.hilt.android.work", "HiltWorker");
144+
public static final ClassName HILT_WORKER_MAP_QUALIFIER =
145+
get("dagger.hilt.android.internal.work", "HiltWorkerMap");
146+
public static final ClassName HILT_WORKER_KEYS_QUALIFIER =
147+
get("dagger.hilt.android.internal.work", "HiltWorkerMap", "KeySet");
148+
140149
private AndroidClassNames() {}
141150
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright (C) 2024 The Dagger Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package dagger.hilt.android.processor.internal.worker
18+
19+
import androidx.room3.compiler.codegen.toJavaPoet
20+
import androidx.room3.compiler.processing.ExperimentalProcessingApi
21+
import androidx.room3.compiler.processing.XProcessingEnv
22+
import androidx.room3.compiler.processing.XTypeElement
23+
import com.squareup.javapoet.ClassName
24+
import dagger.hilt.android.processor.internal.AndroidClassNames
25+
import dagger.hilt.processor.internal.ClassNames
26+
import dagger.hilt.processor.internal.LazyString
27+
import dagger.hilt.processor.internal.ProcessorErrors
28+
import dagger.hilt.processor.internal.Processors
29+
import dagger.internal.codegen.xprocessing.XAnnotations
30+
import dagger.internal.codegen.xprocessing.XElements
31+
import dagger.internal.codegen.xprocessing.XTypes
32+
33+
@OptIn(
34+
ExperimentalProcessingApi::class,
35+
com.squareup.kotlinpoet.javapoet.KotlinPoetJavaPoetPreview::class
36+
)
37+
internal class HiltWorkerMetadata
38+
private constructor(
39+
val workerElement: XTypeElement,
40+
val isAssistedInject: Boolean,
41+
) {
42+
val className = workerElement.asClassName().toJavaPoet()
43+
44+
val assistedFactoryClassName: ClassName =
45+
ClassName.get(workerElement.packageName, "${className.simpleNames().joinToString("_")}_AssistedFactory")
46+
47+
val modulesClassName =
48+
ClassName.get(
49+
workerElement.packageName,
50+
"${className.simpleNames().joinToString("_")}_HiltModules",
51+
)
52+
53+
companion object {
54+
internal fun create(
55+
processingEnv: XProcessingEnv,
56+
workerElement: XTypeElement,
57+
): HiltWorkerMetadata? {
58+
ProcessorErrors.checkState(
59+
XTypes.isSubtype(
60+
workerElement.type,
61+
processingEnv.requireType(AndroidClassNames.LISTENABLE_WORKER),
62+
),
63+
workerElement,
64+
"@HiltWorker is only supported on types that subclass %s.",
65+
AndroidClassNames.LISTENABLE_WORKER,
66+
)
67+
68+
val injectConstructors =
69+
workerElement.getConstructors().filter { constructor ->
70+
Processors.isAnnotatedWithInject(constructor) ||
71+
constructor.hasAnnotation(ClassNames.ASSISTED_INJECT)
72+
}
73+
74+
ProcessorErrors.checkState(
75+
injectConstructors.size == 1,
76+
workerElement,
77+
"@HiltWorker annotated class should contain exactly one @Inject or @AssistedInject annotated constructor.",
78+
)
79+
80+
val injectConstructor = injectConstructors.single()
81+
82+
ProcessorErrors.checkState(
83+
!injectConstructor.isPrivate(),
84+
injectConstructor,
85+
"%s annotated constructors must not be private.",
86+
if (injectConstructor.hasAnnotation(ClassNames.ASSISTED_INJECT)) {
87+
"@Inject or @AssistedInject"
88+
} else {
89+
"@Inject"
90+
},
91+
)
92+
93+
ProcessorErrors.checkState(
94+
!workerElement.isNested() || workerElement.isStatic(),
95+
workerElement,
96+
"@HiltWorker may only be used on inner classes if they are static.",
97+
)
98+
99+
Processors.getScopeAnnotations(workerElement).let { scopeAnnotations ->
100+
ProcessorErrors.checkState(
101+
scopeAnnotations.isEmpty(),
102+
workerElement,
103+
"@HiltWorker classes should not be scoped. Found: %s",
104+
LazyString.of { scopeAnnotations.joinToString { XAnnotations.toStableString(it) } },
105+
)
106+
}
107+
108+
val isAssistedInject = injectConstructor.hasAnnotation(ClassNames.ASSISTED_INJECT)
109+
110+
return HiltWorkerMetadata(workerElement, isAssistedInject)
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)