Skip to content

Commit

Permalink
Update Hilt's ViewModel APIs to be a type annotation along with @Inject
Browse files Browse the repository at this point in the history
… constructor annotation. This solidifies the relationship between Hilt/Dagger and ViewModels and will allow Dagger to generated the factories that will construction inject and member inject the ViewModels.

RELNOTES=Update @ViewModelInject API to @HiltViewModel
PiperOrigin-RevId: 346245747
  • Loading branch information
java-team-github-bot authored and Dagger Team committed Dec 8, 2020
1 parent d3c17de commit 253ac8b
Show file tree
Hide file tree
Showing 28 changed files with 283 additions and 315 deletions.
2 changes: 1 addition & 1 deletion java/dagger/hilt/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ filegroup(
"//java/dagger/hilt/android/processor/internal/bindvalue:srcs_filegroup",
"//java/dagger/hilt/android/processor/internal/customtestapplication:srcs_filegroup",
"//java/dagger/hilt/android/processor/internal/uninstallmodules:srcs_filegroup",
"//java/dagger/hilt/android/processor/internal/viewmodelinject:srcs_filegroup",
"//java/dagger/hilt/android/processor/internal/viewmodel:srcs_filegroup",
"//java/dagger/hilt/processor:srcs_filegroup",
"//java/dagger/hilt/processor/internal:srcs_filegroup",
"//java/dagger/hilt/processor/internal/aggregateddeps:srcs_filegroup",
Expand Down
13 changes: 6 additions & 7 deletions java/dagger/hilt/android/components/ViewModelComponent.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@
* A Hilt component that has the lifetime of a single {@link androidx.lifecycle.ViewModel}.
*
* <p>This Hilt component is the source of {@link
* dagger.hilt.android.lifecycle.ViewModelInject}-annotated {@link
* androidx.lifecycle.ViewModel}s used by the {@link
* dagger.hilt.android.lifecycle.HiltViewModelFactory}. It contains a default binding of the {@link
* androidx.lifecycle.SavedStateHandle} associated with the {@code ViewModel} that can be used
* by other dependencies provided by the component.
* dagger.hilt.android.lifecycle.HiltViewModel}-annotated {@link androidx.lifecycle.ViewModel}s
* used by the {@link dagger.hilt.android.lifecycle.HiltViewModelFactory}. It contains a default
* binding for the {@link androidx.lifecycle.SavedStateHandle} associated with the {@code
* ViewModel} that can be used by other dependencies provided by the component.
*
* <p>Dependencies available in the {@link dagger.hilt.components.SingletonComponent} and {@link
* ActivityRetainedComponent} are also available in this component since it is a child of {@code
Expand All @@ -48,10 +47,10 @@
*
* <p>Dependencies in the {@code ViewModelComponent} can be scoped using the {@link ViewModelScoped}
* annotation. This allows for a single instance of a dependency to be provided across the
* dependencies of a single {@link dagger.hilt.android.lifecycle.ViewModelInject}-annotated {@code
* dependencies of a single {@link dagger.hilt.android.lifecycle.HiltViewModel}-annotated {@code
* ViewModel}.
*
* @see dagger.hilt.android.lifecycle.ViewModelInject
* @see dagger.hilt.android.lifecycle.HiltViewModel
* @see dagger.hilt.android.scopes.ViewModelScoped
*/
@ViewModelScoped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
@Qualifier
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.PARAMETER})
public @interface ViewModelInjectMap {
public @interface HiltViewModelMap {

/** Internal qualifier for the multibinding set of class names annotated with @ViewModelInject. */
@Qualifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import dagger.hilt.android.internal.builders.ViewModelComponentBuilder;
import dagger.hilt.android.internal.lifecycle.DefaultActivityViewModelFactory;
import dagger.hilt.android.internal.lifecycle.DefaultFragmentViewModelFactory;
import dagger.hilt.android.internal.lifecycle.ViewModelInjectMap;
import dagger.hilt.android.internal.lifecycle.HiltViewModelMap;
import dagger.hilt.android.lifecycle.HiltViewModelFactory;
import dagger.multibindings.IntoSet;
import dagger.multibindings.Multibinds;
Expand All @@ -51,7 +51,7 @@ public final class ViewModelFactoryModules {
public abstract static class ViewModelModule {
@NonNull
@Multibinds
@ViewModelInjectMap
@HiltViewModelMap
abstract Map<String, ViewModel> viewModelFactoriesMap();
}

Expand All @@ -64,7 +64,7 @@ public abstract static class ViewModelModule {
public abstract static class ActivityRetainedModule {
@NonNull
@Multibinds
@ViewModelInjectMap.KeySet
@HiltViewModelMap.KeySet
abstract Set<String> viewModelKeys();
}

Expand All @@ -81,7 +81,7 @@ static ViewModelProvider.Factory provideFactory(
@NonNull Activity activity,
@NonNull
Application application,
@NonNull @ViewModelInjectMap.KeySet Set<String> keySet,
@NonNull @HiltViewModelMap.KeySet Set<String> keySet,
@NonNull ViewModelComponentBuilder componentBuilder) {
// Hilt guarantees concrete activity is a subclass of ComponentActivity.
ComponentActivity componentActivity = (ComponentActivity) activity;
Expand All @@ -108,7 +108,7 @@ static ViewModelProvider.Factory provideFactory(
@NonNull Fragment fragment,
@NonNull
Application application,
@NonNull @ViewModelInjectMap.KeySet Set<String> keySet,
@NonNull @HiltViewModelMap.KeySet Set<String> keySet,
@NonNull ViewModelComponentBuilder componentBuilder) {
Bundle defaultArgs = fragment.getArguments();
SavedStateViewModelFactory delegate =
Expand Down
3 changes: 2 additions & 1 deletion java/dagger/hilt/android/lifecycle/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ android_library(
name = "lifecycle",
srcs = glob(["*.java"]),
exported_plugins = [
"//java/dagger/hilt/android/processor/internal/viewmodelinject:processor",
"//java/dagger/hilt/android/processor/internal/viewmodel:processor",
],
proguard_specs = ["proguard-rules.pro"],
exports = [
"//java/dagger/hilt/android/components:view_model_component",
"//java/dagger/hilt/android/internal/lifecycle",
],
deps = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,21 @@
import java.lang.annotation.Target;

/**
* Identifies a {@link androidx.lifecycle.ViewModel}'s constructor for injection.
* Identifies a {@link androidx.lifecycle.ViewModel} for construction injection.
*
* <p>Similar to {@link javax.inject.Inject}, a {@code ViewModel} containing a constructor annotated
* with {@code ViewModelInject} will have its dependencies defined in the constructor parameters
* injected by Dagger's Hilt. The {@code ViewModel} will be available for creation by the {@link
* dagger.hilt.android.lifecycle.HiltViewModelFactory} and can be retrieved by default in an {@code
* Activity} or {@code Fragment} annotated with {@link dagger.hilt.android.AndroidEntryPoint}.
* <p>The {@code ViewModel} annotated with {@link HiltViewModel} will be available for creation by
* the {@link dagger.hilt.android.lifecycle.HiltViewModelFactory} and can be retrieved by default in
* an {@code Activity} or {@code Fragment} annotated with {@link
* dagger.hilt.android.AndroidEntryPoint}. The {@code HiltViewModel} containing a constructor
* annotated with {@link javax.inject.Inject} will have its dependencies defined in the constructor
* parameters injected by Dagger's Hilt.
*
* <p>Example:
*
* <pre>
* &#64;HiltViewModel
* public class DonutViewModel extends ViewModel {
* &#64;ViewModelInject
* &#64;Inject
* public DonutViewModel(SavedStateHandle handle, RecipeRepository repository) {
* // ...
* }
Expand All @@ -51,7 +53,7 @@
* }
* </pre>
*
* <p>Only one constructor in the {@code ViewModel} must be annotated with {@code ViewModelInject}.
* <p>Exactly one constructor in the {@code ViewModel} must be annotated with {@code Inject}.
*
* <p>Only dependencies available in the {@link dagger.hilt.android.components.ViewModelComponent}
* can be injected into the {@code ViewModel}.
Expand All @@ -60,7 +62,7 @@
*
* @see dagger.hilt.android.components.ViewModelComponent
*/
@Target(ElementType.CONSTRUCTOR)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@GeneratesRootInput
public @interface ViewModelInject {}
public @interface HiltViewModel {}
4 changes: 2 additions & 2 deletions java/dagger/hilt/android/lifecycle/HiltViewModelFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import dagger.hilt.InstallIn;
import dagger.hilt.android.components.ViewModelComponent;
import dagger.hilt.android.internal.builders.ViewModelComponentBuilder;
import dagger.hilt.android.internal.lifecycle.ViewModelInjectMap;
import dagger.hilt.android.internal.lifecycle.HiltViewModelMap;
import java.util.Map;
import java.util.Set;
import javax.inject.Provider;
Expand All @@ -48,7 +48,7 @@ public final class HiltViewModelFactory implements ViewModelProvider.Factory {
@EntryPoint
@InstallIn(ViewModelComponent.class)
interface ViewModelFactoriesEntryPoint {
@ViewModelInjectMap
@HiltViewModelMap
Map<String, Provider<ViewModel>> getHiltViewModelInjectMap();
}

Expand Down
5 changes: 1 addition & 4 deletions java/dagger/hilt/android/lifecycle/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
# Keep class names of Hilt injected ViewModels since their name are used as a multibinding map key.
-keepclasseswithmembernames class * extends androidx.lifecycle.ViewModel {
@dagger.hilt.android.lifecycle.ViewModelInject
<init>(...);
}
-keepnames @dagger.hilt.android.lifecycle.HiltViewModel class * extends androidx.lifecycle.ViewModel
2 changes: 1 addition & 1 deletion java/dagger/hilt/android/processor/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ gen_maven_artifact(
"//java/dagger/hilt/android/processor/internal/bindvalue:bind_value_processor_lib",
"//java/dagger/hilt/android/processor/internal/customtestapplication:processor_lib",
"//java/dagger/hilt/android/processor/internal/uninstallmodules:processor_lib",
"//java/dagger/hilt/android/processor/internal/viewmodelinject:processor_lib",
"//java/dagger/hilt/android/processor/internal/viewmodel:processor_lib",
"//java/dagger/hilt/codegen:originating_element",
"//java/dagger/hilt/codegen:package_info",
"//java/dagger/hilt/processor/internal:base_processor",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,17 @@ public final class AndroidClassNames {
public static final ClassName APPLICATION_CONTEXT_MODULE =
get("dagger.hilt.android.internal.modules", "ApplicationContextModule");

public static final ClassName DEFAULT_VIEW_MODEL_FACTORIES =
get("dagger.hilt.android.internal.lifecycle", "DefaultViewModelFactories");
public static final ClassName HILT_VIEW_MODEL =
get("dagger.hilt.android.lifecycle", "HiltViewModel");
public static final ClassName HILT_VIEW_MODEL_MAP_QUALIFIER =
get("dagger.hilt.android.internal.lifecycle", "HiltViewModelMap");
public static final ClassName HILT_VIEW_MODEL_KEYS_QUALIFIER =
get("dagger.hilt.android.internal.lifecycle", "HiltViewModelMap", "KeySet");
public static final ClassName VIEW_MODEL = get("androidx.lifecycle", "ViewModel");
public static final ClassName VIEW_MODEL_INJECT =
get("dagger.hilt.android.lifecycle", "ViewModelInject");
public static final ClassName VIEW_MODEL_INJECT_MAP_QUALIFIER =
get("dagger.hilt.android.internal.lifecycle", "ViewModelInjectMap");
public static final ClassName VIEW_MODEL_INJECT_MAP_KEYS_QUALIFIER =
get("dagger.hilt.android.internal.lifecycle", "ViewModelInjectMap", "KeySet");
public static final ClassName VIEW_MODEL_PROVIDER_FACTORY =
get("androidx.lifecycle", "ViewModelProvider", "Factory");
public static final ClassName DEFAULT_VIEW_MODEL_FACTORIES =
get("dagger.hilt.android.internal.lifecycle", "DefaultViewModelFactories");
public static final ClassName SAVED_STATE_HANDLE =
get("androidx.lifecycle", "SavedStateHandle");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,24 @@ package(default_visibility = ["//:src"])
java_plugin(
name = "processor",
generates_api = 1,
processor_class = "dagger.hilt.android.processor.internal.viewmodelinject.ViewModelInjectProcessor",
processor_class = "dagger.hilt.android.processor.internal.viewmodel.ViewModelProcessor",
deps = [":processor_lib"],
)

kt_jvm_library(
name = "processor_lib",
srcs = [
"ViewModelInjectMetadata.kt",
"ViewModelInjectProcessor.kt",
"ViewModelMetadata.kt",
"ViewModelModuleGenerator.kt",
"ViewModelProcessor.kt",
],
deps = [
"//java/dagger/hilt/android/processor/internal:android_classnames",
"//java/dagger/hilt/processor/internal:base_processor",
"//java/dagger/hilt/processor/internal:classnames",
"//java/dagger/hilt/processor/internal:processor_errors",
"//java/dagger/hilt/processor/internal:processors",
"//java/dagger/internal/guava:collect",
"@google_bazel_common//third_party/java/auto:service",
"@google_bazel_common//third_party/java/incap",
"@google_bazel_common//third_party/java/javapoet",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,25 @@
* limitations under the License.
*/

package dagger.hilt.android.processor.internal.viewmodelinject
package dagger.hilt.android.processor.internal.viewmodel

import com.google.auto.common.MoreElements
import com.squareup.javapoet.AnnotationSpec
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.TypeName
import dagger.hilt.android.processor.internal.AndroidClassNames
import dagger.hilt.processor.internal.ClassNames
import dagger.hilt.processor.internal.ProcessorErrors
import dagger.hilt.processor.internal.Processors
import javax.annotation.processing.ProcessingEnvironment
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.element.NestingKind
import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement
import javax.lang.model.util.ElementFilter

/**
* Data class that represents a Hilt injected ViewModel
*/
internal class ViewModelInjectMetadata private constructor(
val typeElement: TypeElement,
val constructorElement: ExecutableElement
internal class ViewModelMetadata private constructor(
val typeElement: TypeElement
) {
val className = ClassName.get(typeElement)

Expand All @@ -46,16 +41,11 @@ internal class ViewModelInjectMetadata private constructor(
"${className.simpleNames().joinToString("_")}_HiltModules"
)

val dependencyRequests = constructorElement.parameters.map { constructorArg ->
constructorArg.toDependencyRequest()
}

companion object {
internal fun create(
processingEnv: ProcessingEnvironment,
typeElement: TypeElement,
constructorElement: ExecutableElement
): ViewModelInjectMetadata? {
): ViewModelMetadata? {
val types = processingEnv.typeUtils
val elements = processingEnv.elementUtils

Expand All @@ -65,24 +55,30 @@ internal class ViewModelInjectMetadata private constructor(
elements.getTypeElement(AndroidClassNames.VIEW_MODEL.toString()).asType()
),
typeElement,
"@ViewModelInject is only supported on types that subclass %s.",
"@HiltViewModel is only supported on types that subclass %s.",
AndroidClassNames.VIEW_MODEL
)

ElementFilter.constructorsIn(typeElement.enclosedElements).filter {
Processors.hasAnnotation(it, AndroidClassNames.VIEW_MODEL_INJECT)
}.let { constructors ->
ElementFilter.constructorsIn(typeElement.enclosedElements).filter { constructor ->
ProcessorErrors.checkState(
!Processors.hasAnnotation(constructor, ClassNames.ASSISTED_INJECT),
constructor,
"ViewModel constructor should be annotated with @Inject instead of @AssistedInject."
)
Processors.hasAnnotation(constructor, ClassNames.INJECT)
}.let { injectConstructors ->
ProcessorErrors.checkState(
constructors.size == 1,
injectConstructors.size == 1,
typeElement,
"Multiple @ViewModelInject annotated constructors found."
"@HiltViewModel annotated class should contain exactly one @Inject " +
"annotated constructor."
)

constructors.forEach { constructor ->
injectConstructors.forEach { constructor ->
ProcessorErrors.checkState(
!constructor.modifiers.contains(Modifier.PRIVATE),
constructor,
"@ViewModelInject annotated constructors must not be private."
"@Inject annotated constructors must not be private."
)
}
}
Expand All @@ -91,46 +87,21 @@ internal class ViewModelInjectMetadata private constructor(
typeElement.nestingKind != NestingKind.MEMBER ||
typeElement.modifiers.contains(Modifier.STATIC),
typeElement,
"@ViewModelInject may only be used on inner classes if they are static."
"@HiltViewModel may only be used on inner classes if they are static."
)

// Validate there is at most one SavedStateHandle constructor arg.
constructorElement.parameters.filter {
TypeName.get(it.asType()) == AndroidClassNames.SAVED_STATE_HANDLE
}.let { savedStateHandleParams ->
Processors.getScopeAnnotations(typeElement).let { scopeAnnotations ->
ProcessorErrors.checkState(
savedStateHandleParams.size <= 1,
constructorElement,
"Expected zero or one constructor argument of type %s, found %s",
AndroidClassNames.SAVED_STATE_HANDLE, savedStateHandleParams.size
scopeAnnotations.isEmpty(),
typeElement,
"@HiltViewModel classes should not be scoped. Found: %s",
scopeAnnotations.joinToString()
)
}

return ViewModelInjectMetadata(
typeElement,
constructorElement
return ViewModelMetadata(
typeElement
)
}
}
}

/**
* Data class that represents a binding request for an injected type.
*/
internal data class DependencyRequest(
val name: String,
val type: TypeName,
val qualifier: AnnotationSpec? = null
)

internal fun VariableElement.toDependencyRequest(): DependencyRequest {
val qualifier = annotationMirrors.find {
Processors.hasAnnotation(it.annotationType.asElement(), ClassNames.QUALIFIER)
}?.let { AnnotationSpec.get(it) }
val type = TypeName.get(asType())
return DependencyRequest(
name = simpleName.toString(),
type = type,
qualifier = qualifier
)
}

0 comments on commit 253ac8b

Please sign in to comment.