From f84384fda86f5f608095d625206cfa096a850e04 Mon Sep 17 00:00:00 2001 From: Eli Hart Date: Thu, 13 Sep 2018 11:17:27 -0500 Subject: [PATCH] Add databinding option to not auto apply DoNotHash (#539) --- .../airbnb/epoxy/EpoxyDataBindingLayouts.java | 11 + .../airbnb/epoxy/EpoxyDataBindingPattern.java | 11 + .../airbnb/epoxy/DataBindingAttributeInfo.kt | 4 +- .../com/airbnb/epoxy/DataBindingModelInfo.kt | 10 +- .../com/airbnb/epoxy/DataBindingProcessor.kt | 95 +++--- .../main/java/com/airbnb/epoxy/KotlinUtils.kt | 2 + ...el_with_data_binding_without_donothash.xml | 22 ++ .../airbnb/epoxy/DataBindingModelTest.java | 25 ++ ...ithDataBindingWithoutDonothashBinding.java | 169 +++++++++++ ...aBindingWithoutDonothashBindingModel_.java | 270 ++++++++++++++++++ 10 files changed, 575 insertions(+), 44 deletions(-) create mode 100644 epoxy-processortest/src/main/res/layout/model_with_data_binding_without_donothash.xml create mode 100644 epoxy-processortest/src/test/resources/ModelWithDataBindingWithoutDonothashBinding.java create mode 100644 epoxy-processortest/src/test/resources/ModelWithDataBindingWithoutDonothashBindingModel_.java diff --git a/epoxy-annotations/src/main/java/com/airbnb/epoxy/EpoxyDataBindingLayouts.java b/epoxy-annotations/src/main/java/com/airbnb/epoxy/EpoxyDataBindingLayouts.java index 88ab0cb41f..21a9a836bc 100644 --- a/epoxy-annotations/src/main/java/com/airbnb/epoxy/EpoxyDataBindingLayouts.java +++ b/epoxy-annotations/src/main/java/com/airbnb/epoxy/EpoxyDataBindingLayouts.java @@ -23,4 +23,15 @@ public @interface EpoxyDataBindingLayouts { /** A list of databinding layout resources that should have EpoxyModel's generated for them. */ @LayoutRes int[] value(); + + /** + * If true, any variable whose type does not implement equals and hashcode will have the + * {@link EpoxyAttribute.Option#DoNotHash} behavior applied to them automatically. + *

+ * This is generally helpful for listeners - other variables should almost always implement + * equals and hashcode. + *

+ * For details on the nuances of this, see https://github.com/airbnb/epoxy/wiki/DoNotHash + */ + boolean enableDoNotHash() default true; } diff --git a/epoxy-annotations/src/main/java/com/airbnb/epoxy/EpoxyDataBindingPattern.java b/epoxy-annotations/src/main/java/com/airbnb/epoxy/EpoxyDataBindingPattern.java index 60bb96bcd8..c4f42d3cf8 100644 --- a/epoxy-annotations/src/main/java/com/airbnb/epoxy/EpoxyDataBindingPattern.java +++ b/epoxy-annotations/src/main/java/com/airbnb/epoxy/EpoxyDataBindingPattern.java @@ -29,4 +29,15 @@ * databinding layout, Epoxy will generate a HeaderBindingModel_ class for that layout. */ String layoutPrefix(); + + /** + * If true, any variable whose type does not implement equals and hashcode will have the + * {@link EpoxyAttribute.Option#DoNotHash} behavior applied to them automatically. + *

+ * This is generally helpful for listeners - other variables should almost always implement + * equals and hashcode. + *

+ * For details on the nuances of this, see https://github.com/airbnb/epoxy/wiki/DoNotHash + */ + boolean enableDoNotHash() default true; } diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/DataBindingAttributeInfo.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/DataBindingAttributeInfo.kt index 1f8d8836cd..6274c5bc24 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/DataBindingAttributeInfo.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/DataBindingAttributeInfo.kt @@ -14,8 +14,8 @@ internal class DataBindingAttributeInfo( typeMirror = setterMethod.parameters[0].asType() modelName = modelInfo.generatedName.simpleName() packageName = modelInfo.generatedName.packageName() - useInHash = hashCodeValidator.implementsHashCodeAndEquals(typeMirror) - ignoreRequireHashCode = false + useInHash = !modelInfo.enableDoNotHash || hashCodeValidator.implementsHashCodeAndEquals(typeMirror) + ignoreRequireHashCode = true generateSetter = true generateGetter = true hasFinalModifier = false diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/DataBindingModelInfo.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/DataBindingModelInfo.kt index 6c02e0badd..cf68551066 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/DataBindingModelInfo.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/DataBindingModelInfo.kt @@ -13,7 +13,8 @@ internal class DataBindingModelInfo( private val elementUtils: Elements, val layoutResource: ResourceValue, val moduleName: String, - private val layoutPrefix: String = "" + private val layoutPrefix: String = "", + val enableDoNotHash: Boolean ) : GeneratedModelInfo() { private val dataBindingClassName: ClassName @@ -46,8 +47,8 @@ internal class DataBindingModelInfo( // This databinding class won't exist until the second round of annotation processing since // it is generated in the first round. val dataBindingClassElement = this.dataBindingClassElement ?: return false - val hashCodeValidator = HashCodeValidator(typeUtils, elementUtils) + dataBindingClassElement.executableElements() .filter { Utils.isSetterMethod(it) } .map { @@ -57,6 +58,7 @@ internal class DataBindingModelInfo( .let { addAttributes(it) } + return true } @@ -66,7 +68,7 @@ internal class DataBindingModelInfo( ): ClassName { val modelName = layoutResource.resourceName!!.toUpperCamelCase().plus(BINDING_SUFFIX) - return ClassName.get(moduleName + ".databinding", modelName) + return ClassName.get("$moduleName.databinding", modelName) } private fun buildGeneratedModelName(): ClassName { @@ -81,7 +83,7 @@ internal class DataBindingModelInfo( companion object { - val BINDING_SUFFIX = "Binding" + const val BINDING_SUFFIX = "Binding" val FIELD_NAME_BLACKLIST = listOf( // Starting with Android plugin 3.1.0 nested DataBinding classes have a diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/DataBindingProcessor.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/DataBindingProcessor.kt index 7eedab63f9..4d17ea69c3 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/DataBindingProcessor.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/DataBindingProcessor.kt @@ -1,20 +1,21 @@ package com.airbnb.epoxy -import com.airbnb.epoxy.Utils.* -import com.squareup.javapoet.* +import com.airbnb.epoxy.Utils.getClassParamFromAnnotation +import com.squareup.javapoet.ClassName import java.util.* -import javax.annotation.processing.* -import javax.lang.model.element.* -import javax.lang.model.util.* +import javax.annotation.processing.RoundEnvironment +import javax.lang.model.element.VariableElement +import javax.lang.model.util.Elements +import javax.lang.model.util.Types internal class DataBindingProcessor( - private val elements: Elements, - private val types: Types, - private val errorLogger: ErrorLogger, - private val configManager: ConfigManager, - private val resourceProcessor: ResourceProcessor, - private val dataBindingModuleLookup: DataBindingModuleLookup, - private val modelWriter: GeneratedModelWriter + private val elements: Elements, + private val types: Types, + private val errorLogger: ErrorLogger, + private val configManager: ConfigManager, + private val resourceProcessor: ResourceProcessor, + private val dataBindingModuleLookup: DataBindingModuleLookup, + private val modelWriter: GeneratedModelWriter ) { private val modelsToWrite = ArrayList() @@ -23,40 +24,58 @@ internal class DataBindingProcessor( roundEnv.getElementsAnnotatedWith(EpoxyDataBindingLayouts::class.java).forEach { val layoutResources = resourceProcessor - .getLayoutsInAnnotation(it, EpoxyDataBindingLayouts::class.java) + .getLayoutsInAnnotation(it, EpoxyDataBindingLayouts::class.java) // Get the module name after parsing resources so we can use the resource classes to figure // out the module name val moduleName = dataBindingModuleLookup.getModuleName(it) + val enableDoNotHash = + it.annotation()?.enableDoNotHash == true layoutResources.mapTo(modelsToWrite) { - DataBindingModelInfo(types, elements, it, moduleName) + DataBindingModelInfo( + typeUtils = types, + elementUtils = elements, + layoutResource = it, + moduleName = moduleName, + enableDoNotHash = enableDoNotHash + ) } } - roundEnv.getElementsAnnotatedWith(EpoxyDataBindingPattern::class.java).forEach { - val patternAnnotation = it.getAnnotation(EpoxyDataBindingPattern::class.java) + roundEnv.getElementsAnnotatedWith(EpoxyDataBindingPattern::class.java).forEach { element -> + val patternAnnotation = element.getAnnotation(EpoxyDataBindingPattern::class.java) val layoutPrefix = patternAnnotation.layoutPrefix val rClassName = getClassParamFromAnnotation( - it, - EpoxyDataBindingPattern::class.java, - "rClass", - types) - ?: return@forEach + element, + EpoxyDataBindingPattern::class.java, + "rClass", + types + ) + ?: return@forEach val moduleName = rClassName.packageName() val layoutClassName = ClassName.get(moduleName, "R", "layout") + val enableDoNotHash = + element.annotation()?.enableDoNotHash == true Utils.getElementByName(layoutClassName, elements, types) - .enclosedElements - .filterIsInstance() - .map { it.simpleName.toString() } - .filter { it.startsWith(layoutPrefix) } - .map { ResourceValue(layoutClassName, it, 0 /* value doesn't matter */) } - .mapTo(modelsToWrite) { - DataBindingModelInfo(types, elements, it, moduleName, layoutPrefix) - } + .enclosedElements + .filterIsInstance() + .map { it.simpleName.toString() } + .filter { it.startsWith(layoutPrefix) } + .map { ResourceValue(layoutClassName, it, 0 /* value doesn't matter */) } + .mapTo(modelsToWrite) { + DataBindingModelInfo( + typeUtils = types, + elementUtils = elements, + layoutResource = it, + moduleName = moduleName, + layoutPrefix = layoutPrefix, + enableDoNotHash = enableDoNotHash + ) + } } val modelsWritten = resolveDataBindingClassesAndWriteJava() @@ -79,15 +98,15 @@ internal class DataBindingProcessor( private fun resolveDataBindingClassesAndWriteJava(): List { return modelsToWrite - .filter { it.parseDataBindingClass() } - .onEach { - try { - modelWriter.generateClassForModel(it) - } catch (e: Exception) { - errorLogger.logError(e, "Error generating model classes") - } - - modelsToWrite.remove(it) + .filter { it.parseDataBindingClass() } + .onEach { + try { + modelWriter.generateClassForModel(it) + } catch (e: Exception) { + errorLogger.logError(e, "Error generating model classes") } + + modelsToWrite.remove(it) + } } } diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/KotlinUtils.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/KotlinUtils.kt index dc17638f88..de871c531c 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/KotlinUtils.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/KotlinUtils.kt @@ -159,6 +159,8 @@ fun Element.iterateSuperClasses( } } +inline fun Element.annotation(): T? = getAnnotation(T::class.java) + /** * Returns a list of annotations specs representing annotations on the given type element. * diff --git a/epoxy-processortest/src/main/res/layout/model_with_data_binding_without_donothash.xml b/epoxy-processortest/src/main/res/layout/model_with_data_binding_without_donothash.xml new file mode 100644 index 0000000000..829f70680a --- /dev/null +++ b/epoxy-processortest/src/main/res/layout/model_with_data_binding_without_donothash.xml @@ -0,0 +1,22 @@ + + + + + + + + + + +