Skip to content

Commit

Permalink
Add databinding option to not auto apply DoNotHash (#539)
Browse files Browse the repository at this point in the history
  • Loading branch information
elihart authored Sep 13, 2018
1 parent 8b47562 commit f84384f
Show file tree
Hide file tree
Showing 10 changed files with 575 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* This is generally helpful for listeners - other variables should almost always implement
* equals and hashcode.
* <p>
* For details on the nuances of this, see https://github.com/airbnb/epoxy/wiki/DoNotHash
*/
boolean enableDoNotHash() default true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* This is generally helpful for listeners - other variables should almost always implement
* equals and hashcode.
* <p>
* For details on the nuances of this, see https://github.com/airbnb/epoxy/wiki/DoNotHash
*/
boolean enableDoNotHash() default true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand All @@ -57,6 +58,7 @@ internal class DataBindingModelInfo(
.let {
addAttributes(it)
}

return true
}

Expand All @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DataBindingModelInfo>()

Expand All @@ -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<EpoxyDataBindingLayouts>()?.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<EpoxyDataBindingPattern>(
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<EpoxyDataBindingLayouts>()?.enableDoNotHash == true

Utils.getElementByName(layoutClassName, elements, types)
.enclosedElements
.filterIsInstance<VariableElement>()
.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<VariableElement>()
.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()
Expand All @@ -79,15 +98,15 @@ internal class DataBindingProcessor(

private fun resolveDataBindingClassesAndWriteJava(): List<DataBindingModelInfo> {
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)
}
}
}
2 changes: 2 additions & 0 deletions epoxy-processor/src/main/java/com/airbnb/epoxy/KotlinUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ fun Element.iterateSuperClasses(
}
}

inline fun <reified T : Annotation> Element.annotation(): T? = getAnnotation(T::class.java)

/**
* Returns a list of annotations specs representing annotations on the given type element.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>

<variable
name="stringValue"
type="String" />

<variable
name="clickListener"
type="android.view.View.OnClickListener" />
</data>

<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="40dp"
android:onClick="@{clickListener}"
android:text="@{stringValue}" />

</layout>
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class DataBindingModelTest {
+ " public static final class layout {\n"
+ " public static final int res = 0x7f040008;\n"
+ " public static final int model_with_data_binding=0x7f040009;\n"
+ " public static final int model_with_data_binding_without_donothash=0x7f0f002b;\n"
+ " }\n"
+ " public static final class integer {\n"
+ " public static final int res = 0x7f040004;\n"
Expand Down Expand Up @@ -66,6 +67,7 @@ public class DataBindingModelTest {
+ " public static final int valueObject = 20;\n"
+ " public static final int valueList = 21;\n"
+ " public static final int stringValue = 22;\n"
+ " public static final int clickListener = 23;\n"
+ "}");

@Test
Expand Down Expand Up @@ -118,4 +120,27 @@ public void testFullyGeneratedModel() {
.and()
.generatesSources(generatedModel);
}

@Test
public void testFullyGeneratedModelWithoutDoNotHash() {
JavaFileObject packageInfo = JavaFileObjects
.forSourceString("com.airbnb.epoxy.package-info",
"@EpoxyDataBindingLayouts(value = {R.layout"
+ ".model_with_data_binding_without_donothash}, enableDoNotHash = false)\n"
+ "package com.airbnb.epoxy;\n"
);

JavaFileObject binding = JavaFileObjects
.forResource("ModelWithDataBindingWithoutDonothashBinding.java");

JavaFileObject generatedModel =
JavaFileObjects.forResource("ModelWithDataBindingWithoutDonothashBindingModel_.java");

assert_().about(javaSources())
.that(asList(packageInfo, binding, BR_CLASS, R))
.processedWith(new EpoxyProcessor())
.compilesWithoutError()
.and()
.generatesSources(generatedModel);
}
}
Loading

0 comments on commit f84384f

Please sign in to comment.