diff --git a/.travis.yml b/.travis.yml index ef35e2d144..fc1824368d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,9 @@ android: - tools - platform-tools - build-tools-28.0.3 + - build-tools-29.0.2 - android-28 + - android-29 - extra-google-google_play_services - extra-android-m2repository - extra-google-m2repository diff --git a/CHANGELOG.md b/CHANGELOG.md index 25cf247392..0aa066613f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 4.0.0-beta4 (June 1, 2020) +Fixes: +- Synchronize ListUpdateCallback and PagedListModelCache functions (#987) +- 4.0.0.beta1 generating duplicate method layout(int) #988 + # 4.0.0-beta3 (May 27, 2020) - Sort functions in generated kotlin extension function files deterministically to prevent generated sources from changing - Avoid generating bitset checks in models when not needed diff --git a/build.gradle b/build.gradle index cf985d802f..1bd0a9d118 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.KOTLIN_VERSION = "1.3.72" - ext.ANDROID_PLUGIN_VERSION = '3.6.3' + ext.ANDROID_PLUGIN_VERSION = '4.0.0' repositories { google() @@ -37,22 +37,10 @@ allprojects { subprojects { project -> apply from: "$rootDir/blessedDeps.gradle" apply plugin: 'com.github.ben-manes.versions' - apply plugin: 'checkstyle' apply from: "${project.rootDir}/ktlint.gradle" - task checkstyle(type: Checkstyle) { - configFile rootProject.file('checkstyle.xml') - source 'src/main/java' - ignoreFailures false - showViolations true - include '**/*.java' - - classpath = files() - } - afterEvaluate { if (project.tasks.findByName('check')) { - check.dependsOn('checkstyle') check.dependsOn('ktlint') } } diff --git a/checkstyle.xml b/checkstyle.xml deleted file mode 100644 index 851c13b8a4..0000000000 --- a/checkstyle.xml +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/epoxy-annotations/build.gradle b/epoxy-annotations/build.gradle index ca2a06f5aa..d30ea6b702 100644 --- a/epoxy-annotations/build.gradle +++ b/epoxy-annotations/build.gradle @@ -9,11 +9,6 @@ mavenPublish { sourceCompatibility = rootProject.JAVA_SOURCE_VERSION targetCompatibility = rootProject.JAVA_TARGET_VERSION -checkstyle { - configFile rootProject.file('checkstyle.xml') - showViolations true -} - dependencies { implementation rootProject.deps.androidAnnotations // Allow us to use android support library annotations (@LayoutRes) in this project. diff --git a/epoxy-processor/build.gradle b/epoxy-processor/build.gradle index eebd8d29a9..1a013f722f 100644 --- a/epoxy-processor/build.gradle +++ b/epoxy-processor/build.gradle @@ -42,8 +42,3 @@ dependencies { testImplementation rootProject.deps.junit } - -checkstyle { - configFile rootProject.file('checkstyle.xml') - showViolations true -} diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Memoizer.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Memoizer.kt index e94d161c14..b4a26256c6 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Memoizer.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Memoizer.kt @@ -59,9 +59,9 @@ class Memoizer( getTypeMirror(ClassNames.ANDROID_VIEW, elements, types) } - private val methodsReturningClassType = mutableMapOf>() + private val methodsReturningClassType = mutableMapOf>() - fun getMethodsReturningClassType(classType: TypeMirror): List = + fun getMethodsReturningClassType(classType: TypeMirror): Set = synchronized(methodsReturningClassType) { val classElement = types.asElement(classType) as TypeElement methodsReturningClassType.getOrPut(classElement.qualifiedName) { @@ -71,7 +71,7 @@ class Memoizer( val superClassType = classElement.superclass superClassType.ensureLoaded() // Check for base Object class - if (superClassType.kind == TypeKind.NONE) return@getOrPut emptyList() + if (superClassType.kind == TypeKind.NONE) return@getOrPut emptySet() val methodInfos: List = classElement.enclosedElementsThreadSafe.mapNotNull { subElement -> @@ -114,7 +114,10 @@ class Memoizer( ) } - methodInfos + getMethodsReturningClassType(superClassType) + // Note: Adding super type methods second preserves any overloads in the base + // type that may have changes (ie, a new return type or annotation), since + // Set.plus only adds items that don't already exist. + methodInfos.toSet() + getMethodsReturningClassType(superClassType) } } diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/MethodInfo.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/MethodInfo.kt index a166701187..94dca6e2c8 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/MethodInfo.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/MethodInfo.kt @@ -1,6 +1,7 @@ package com.airbnb.epoxy.processor import com.squareup.javapoet.ParameterSpec +import com.squareup.javapoet.TypeName import javax.lang.model.element.ExecutableElement import javax.lang.model.element.Modifier @@ -11,4 +12,28 @@ data class MethodInfo( val varargs: Boolean, val isEpoxyAttribute: Boolean, val methodElement: ExecutableElement -) +) { + private val paramTypes: List get() = params.map { it.type } + + // Use an equals/hashcode that matches method signature, but doesn't count non signature + // changes such as annotations, return type, or param names. + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MethodInfo + + if (name != other.name) return false + if (paramTypes != other.paramTypes) return false + if (varargs != other.varargs) return false + + return true + } + + override fun hashCode(): Int { + var result = name?.hashCode() ?: 0 + result = 31 * result + paramTypes.hashCode() + result = 31 * result + varargs.hashCode() + return result + } +} diff --git a/epoxy-processortest/src/test/java/com/airbnb/epoxy/ModelProcessorTest.java b/epoxy-processortest/src/test/java/com/airbnb/epoxy/ModelProcessorTest.java deleted file mode 100644 index a09cca8fb7..0000000000 --- a/epoxy-processortest/src/test/java/com/airbnb/epoxy/ModelProcessorTest.java +++ /dev/null @@ -1,372 +0,0 @@ -package com.airbnb.epoxy; - -import com.google.testing.compile.JavaFileObjects; - -import org.junit.Test; - -import javax.tools.JavaFileObject; - -import static com.airbnb.epoxy.ProcessorTestUtils.assertGenerationError; -import static com.airbnb.epoxy.ProcessorTestUtils.checkFileCompiles; -import static com.airbnb.epoxy.ProcessorTestUtils.assertGeneration; -import static com.airbnb.epoxy.ProcessorTestUtils.processors; -import static com.google.common.truth.Truth.assert_; -import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; -import static junit.framework.Assert.assertTrue; - -/** - * These processor tests are in their own module since the processor module can't depend on the - * android EpoxyAdapter library that contains the EpoxyModel. - */ - -@SuppressWarnings("ResultOfMethodCallIgnored") -public class ModelProcessorTest { - @Test - public void testSimpleModel() { - assertGeneration("BasicModelWithAttribute.java", "BasicModelWithAttribute_.java"); - } - - @Test - public void testModelWithAllFieldTypes() { - assertGeneration("ModelWithAllFieldTypes.java", "ModelWithAllFieldTypes_.java"); - } - - @Test - public void testModelWithConstructors() { - assertGeneration("ModelWithConstructors.java", "ModelWithConstructors_.java"); - } - - @Test - public void testModelWithSuper() { - assertGeneration("ModelWithSuper.java", "ModelWithSuper_.java"); - } - - @Test - public void testModelWithFieldAnnotation() { - assertGeneration("ModelWithFieldAnnotation.java", "ModelWithFieldAnnotation_.java"); - } - - @Test - public void testModelWithSuperClass() { - JavaFileObject model = JavaFileObjects - .forResource(GuavaPatch.patchResource("ModelWithSuperAttributes.java")); - - JavaFileObject generatedModel = JavaFileObjects.forResource(GuavaPatch.patchResource("ModelWithSuperAttributes_.java")); - JavaFileObject generatedSubClassModel = - JavaFileObjects.forResource(GuavaPatch.patchResource("ModelWithSuperAttributes$SubModelWithSuperAttributes_.java")); - - assert_().about(javaSource()) - .that(model) - .processedWith(processors()) - .compilesWithoutError() - .and() - .generatesSources(generatedModel, generatedSubClassModel); - } - - @Test - public void testModelWithType() { - assertGeneration("ModelWithType.java", "ModelWithType_.java"); - } - - @Test - public void testModelWithoutHash() { - assertGeneration("ModelWithoutHash.java", "ModelWithoutHash_.java"); - } - - @Test - public void testDoNotHashModel() { - assertGeneration("ModelDoNotHash.java", "ModelDoNotHash_.java"); - } - - @Test - public void testModelWithFinalAttribute() { - assertGeneration("ModelWithFinalField.java", "ModelWithFinalField_.java"); - } - - @Test - public void testModelWithStaticAttributeFails() { - assertGenerationError("ModelWithStaticField.java", "static"); - } - - @Test - public void testModelWithPrivateClassFails() { - assertGenerationError("ModelWithPrivateInnerClass.java", "private classes"); - } - - @Test - public void testModelWithFinalClassFails() { - assertGenerationError("ModelWithFinalClass.java", ""); - } - - @Test - public void testModelThatDoesNotExtendEpoxyModelFails() { - assertGenerationError("ModelWithoutEpoxyExtension.java", "must extend"); - } - - @Test - public void testModelAsInnerClassFails() { - assertGenerationError("ModelAsInnerClass.java", "Nested classes"); - } - - @Test - public void testModelWithIntDefAnnotation() { - assertGeneration("ModelWithIntDef.java", "ModelWithIntDef_.java"); - } - - @Test - public void testModelWithAnnotatedClass() { - assertGeneration("ModelWithAnnotatedClass.java", "ModelWithAnnotatedClass_.java"); - } - - @Test - public void testModelWithAbstractClass() { - JavaFileObject model = JavaFileObjects - .forResource(GuavaPatch.patchResource("ModelWithAbstractClass.java")); - - assert_().about(javaSource()) - .that(model) - .processedWith(processors()) - .compilesWithoutError(); - - // We don't generate subclasses if the model is abstract unless it has a class annotation. - boolean modelNotGenerated; - try { - JavaFileObjects.forResource("non-sense? Any wrong path will generate IAE"); - modelNotGenerated = false; - } catch (IllegalArgumentException e) { - modelNotGenerated = true; - } - - assertTrue(modelNotGenerated); - } - - @Test - public void testModelWithAbstractClassAndAnnotation() { - assertGeneration("ModelWithAbstractClassAndAnnotation.java", - "ModelWithAbstractClassAndAnnotation_.java"); - } - - @Test - public void testModelWithAnnotatedClassAndSuperClass() { - JavaFileObject model = JavaFileObjects - .forResource(GuavaPatch.patchResource("ModelWithAnnotatedClassAndSuperAttributes.java")); - - JavaFileObject generatedModel = JavaFileObjects - .forResource(GuavaPatch.patchResource("ModelWithAnnotatedClassAndSuperAttributes_.java")); - JavaFileObject generatedSubClassModel = - JavaFileObjects.forResource(GuavaPatch.patchResource( - "ModelWithAnnotatedClassAndSuperAttributes$SubModel" - + "WithAnnotatedClassAndSuperAttributes_.java")); - - assert_().about(javaSource()) - .that(model) - .processedWith(processors()) - .compilesWithoutError() - .and() - .generatesSources(generatedModel, generatedSubClassModel); - } - - @Test - public void testModelWithoutSetter() { - assertGeneration("ModelWithoutSetter.java", "ModelWithoutSetter_.java"); - } - - @Test - public void testModelReturningClassType() { - assertGeneration("ModelReturningClassType.java", "ModelReturningClassType_.java"); - } - - @Test - public void testModelReturningClassTypeWithVarargs() { - assertGeneration("ModelReturningClassTypeWithVarargs.java", - "ModelReturningClassTypeWithVarargs_.java"); - } - - @Test - public void testModelWithVarargsConstructors() { - assertGeneration("ModelWithVarargsConstructors.java", "ModelWithVarargsConstructors_.java"); - } - - @Test - public void testModelWithHolderGeneratesNewHolderMethod() { - assertGeneration("AbstractModelWithHolder.java", "AbstractModelWithHolder_.java"); - } - - @Test - public void testGenerateDefaultLayoutMethod() { - assertGeneration("GenerateDefaultLayoutMethod.java", "GenerateDefaultLayoutMethod_.java"); - } - - @Test - public void testGenerateDefaultLayoutMethodFailsIfLayoutNotSpecified() { - assertGenerationError("GenerateDefaultLayoutMethodNoLayout.java", - "Model must specify a valid layout resource"); - } - - @Test - public void testGeneratedDefaultMethodWithLayoutSpecifiedInParent() { - JavaFileObject model = JavaFileObjects - .forResource(GuavaPatch.patchResource("GenerateDefaultLayoutMethodParentLayout.java")); - - JavaFileObject generatedNoLayoutModel = JavaFileObjects - .forResource(GuavaPatch.patchResource("GenerateDefaultLayoutMethodParentLayout$NoLayout_.java")); - JavaFileObject generatedWithLayoutModel = - JavaFileObjects.forResource(GuavaPatch.patchResource("GenerateDefaultLayoutMethodParentLayout$WithLayout_.java")); - - assert_().about(javaSource()) - .that(model) - .processedWith(processors()) - .compilesWithoutError() - .and() - .generatesSources(generatedNoLayoutModel, generatedWithLayoutModel); - } - - @Test - public void testGeneratedDefaultMethodWithLayoutSpecifiedInNextParent() { - JavaFileObject model = JavaFileObjects - .forResource(GuavaPatch.patchResource("GenerateDefaultLayoutMethodNextParentLayout.java")); - - JavaFileObject generatedNoLayoutModel = JavaFileObjects - .forResource(GuavaPatch.patchResource("GenerateDefaultLayoutMethodNextParentLayout$NoLayout_.java")); - JavaFileObject generatedStillNoLayoutModel = JavaFileObjects - .forResource(GuavaPatch.patchResource("GenerateDefaultLayoutMethodNextParentLayout$StillNoLayout_.java")); - JavaFileObject generatedWithLayoutModel = - JavaFileObjects.forResource(GuavaPatch.patchResource("GenerateDefaultLayoutMethodNextParentLayout$WithLayout_.java")); - - assert_().about(javaSource()) - .that(model) - .processedWith(processors()) - .compilesWithoutError() - .and() - .generatesSources(generatedNoLayoutModel, generatedStillNoLayoutModel, - generatedWithLayoutModel); - } - - @Test - public void testGeneratedDefaultMethodWithLayoutFailsIfNotSpecifiedInParent() { - assertGenerationError("GenerateDefaultLayoutMethodParentStillNoLayout.java", - "Model must specify a valid layout resource"); - } - - @Test - public void modelWithViewClickListener() { - assertGeneration("ModelWithViewClickListener.java", "ModelWithViewClickListener_.java"); - } - - @Test - public void modelWithViewClickLongListener() { - JavaFileObject model = JavaFileObjects - .forResource(GuavaPatch.patchResource("ModelWithViewLongClickListener.java")); - - JavaFileObject generatedNoLayoutModel = JavaFileObjects - .forResource(GuavaPatch.patchResource("ModelWithViewLongClickListener_.java")); - - assert_().about(javaSource()) - .that(model) - .processedWith(processors()) - .compilesWithoutError() - .and() - .generatesSources(generatedNoLayoutModel); - } - - @Test - public void modelWithCheckedChangeListener() { - assertGeneration("ModelWithCheckedChangeListener.java", "ModelWithCheckedChangeListener_.java"); - } - - @Test - public void testModelWithPrivateAttributeWithoutGetterAndSetterFails() { - assertGenerationError("ModelWithPrivateFieldWithoutGetterAndSetter.java", - "private fields without proper getter and setter"); - } - - @Test - public void testModelWithPrivateAttributeWithoutSetterFails() { - assertGenerationError("ModelWithPrivateFieldWithoutSetter.java", - "private fields without proper getter and setter"); - } - - @Test - public void testModelWithPrivateAttributeWithoutGetterFails() { - assertGenerationError("ModelWithPrivateFieldWithoutGetter.java", - "private fields without proper getter and setter"); - } - - @Test - public void testModelWithPrivateAttributeWithIsPrefixGetter() { - checkFileCompiles("ModelWithPrivateFieldWithIsPrefixGetter.java"); - } - - @Test - public void testModelWithPrivateAttributeWithPrivateGetterFails() { - assertGenerationError("ModelWithPrivateFieldWithPrivateGetter.java", - "private fields without proper getter and setter"); - } - - @Test - public void testModelWithPrivateAttributeWithStaticGetterFails() { - assertGenerationError("ModelWithPrivateFieldWithStaticGetter.java", - "private fields without proper getter and setter"); - } - - @Test - public void testModelWithPrivateAttributeWithGetterWithParamsFails() { - assertGenerationError("ModelWithPrivateFieldWithGetterWithParams.java", - "private fields without proper getter and setter"); - } - - @Test - public void testModelWithPrivateAttributeWithPrivateSetterFails() { - assertGenerationError("ModelWithPrivateFieldWithPrivateSetter.java", - "private fields without proper getter and setter"); - } - - @Test - public void testModelWithPrivateAttributeWithStaticSetterFails() { - assertGenerationError("ModelWithPrivateFieldWithStaticSetter.java", - "private fields without proper getter and setter"); - } - - @Test - public void testModelWithPrivateAttributeWithGetterWithoutParamsFails() { - assertGenerationError("ModelWithPrivateFieldWithSettterWithoutParams.java", - "private fields without proper getter and setter"); - } - - @Test - public void testModelWithAllPrivateFieldTypes() { - assertGeneration("ModelWithAllPrivateFieldTypes.java", "ModelWithAllPrivateFieldTypes_.java"); - } - - @Test - public void modelWithViewPrivateClickListener() { - assertGeneration("ModelWithPrivateViewClickListener.java", - "ModelWithPrivateViewClickListener_.java"); - } - - @Test - public void modelWithPrivateFieldWithSameAsFieldGetterAndSetterName() { - assertGeneration("ModelWithPrivateFieldWithSameAsFieldGetterAndSetterName.java", - "ModelWithPrivateFieldWithSameAsFieldGetterAndSetterName_.java"); - } - - @Test - public void testDoNotUseInToStringModel() { - assertGeneration("ModelDoNotUseInToString.java", "ModelDoNotUseInToString_.java"); - } - - @Test - public void modelWithAnnotation() { - assertGeneration("ModelWithAnnotation.java", "ModelWithAnnotation_.java"); - } - - @Test - public void testModelBuilderInterface() { - assertGeneration("ModelWithAllFieldTypes.java", "ModelWithAllFieldTypesBuilder.java"); - } - - @Test - public void generatedEpoxyModelGroup() { - assertGeneration("EpoxyModelGroupWithAnnotations.java", "EpoxyModelGroupWithAnnotations_.java"); - } -} diff --git a/epoxy-processortest/src/test/java/com/airbnb/epoxy/ModelProcessorTest.kt b/epoxy-processortest/src/test/java/com/airbnb/epoxy/ModelProcessorTest.kt new file mode 100644 index 0000000000..ab550b8459 --- /dev/null +++ b/epoxy-processortest/src/test/java/com/airbnb/epoxy/ModelProcessorTest.kt @@ -0,0 +1,424 @@ +package com.airbnb.epoxy + +import com.airbnb.epoxy.ProcessorTestUtils.assertGeneration +import com.airbnb.epoxy.ProcessorTestUtils.assertGenerationError +import com.airbnb.epoxy.ProcessorTestUtils.checkFileCompiles +import com.airbnb.epoxy.ProcessorTestUtils.processors +import com.google.common.truth.Truth +import com.google.testing.compile.JavaFileObjects +import com.google.testing.compile.JavaSourceSubjectFactory +import junit.framework.Assert +import org.junit.Test + +/** + * These processor tests are in their own module since the processor module can't depend on the + * android EpoxyAdapter library that contains the EpoxyModel. + */ +class ModelProcessorTest { + @Test + fun testSimpleModel() { + assertGeneration("BasicModelWithAttribute.java", "BasicModelWithAttribute_.java") + } + + @Test + fun testModelWithAllFieldTypes() { + assertGeneration("ModelWithAllFieldTypes.java", "ModelWithAllFieldTypes_.java") + } + + @Test + fun testModelWithConstructors() { + assertGeneration("ModelWithConstructors.java", "ModelWithConstructors_.java") + } + + @Test + fun testModelWithSuper() { + assertGeneration("ModelWithSuper.java", "ModelWithSuper_.java") + } + + @Test + fun testModelWithFieldAnnotation() { + assertGeneration("ModelWithFieldAnnotation.java", "ModelWithFieldAnnotation_.java") + } + + @Test + fun testModelWithSuperClass() { + val model = JavaFileObjects.forResource("ModelWithSuperAttributes.java".patchResource()) + + val generatedModel = + JavaFileObjects.forResource("ModelWithSuperAttributes_.java".patchResource()) + + val generatedSubClassModel = + JavaFileObjects.forResource("ModelWithSuperAttributes\$SubModelWithSuperAttributes_.java".patchResource()) + + Truth.assert_() + .about(JavaSourceSubjectFactory.javaSource()) + .that(model) + .processedWith(processors()) + .compilesWithoutError() + .and() + .generatesSources(generatedModel, generatedSubClassModel) + } + + @Test + fun testModelWithType() { + assertGeneration("ModelWithType.java", "ModelWithType_.java") + } + + @Test + fun testModelWithoutHash() { + assertGeneration("ModelWithoutHash.java", "ModelWithoutHash_.java") + } + + @Test + fun testDoNotHashModel() { + assertGeneration("ModelDoNotHash.java", "ModelDoNotHash_.java") + } + + @Test + fun testModelWithFinalAttribute() { + assertGeneration("ModelWithFinalField.java", "ModelWithFinalField_.java") + } + + @Test + fun testModelWithStaticAttributeFails() { + assertGenerationError("ModelWithStaticField.java", "static") + } + + @Test + fun testModelWithPrivateClassFails() { + assertGenerationError("ModelWithPrivateInnerClass.java", "private classes") + } + + @Test + fun testModelWithFinalClassFails() { + assertGenerationError("ModelWithFinalClass.java", "") + } + + @Test + fun testModelThatDoesNotExtendEpoxyModelFails() { + assertGenerationError("ModelWithoutEpoxyExtension.java", "must extend") + } + + @Test + fun testModelAsInnerClassFails() { + assertGenerationError("ModelAsInnerClass.java", "Nested classes") + } + + @Test + fun testModelWithIntDefAnnotation() { + assertGeneration("ModelWithIntDef.java", "ModelWithIntDef_.java") + } + + @Test + fun testModelWithAnnotatedClass() { + assertGeneration("ModelWithAnnotatedClass.java", "ModelWithAnnotatedClass_.java") + } + + @Test + fun testModelWithAbstractClass() { + val model = JavaFileObjects + .forResource("ModelWithAbstractClass.java".patchResource()) + + Truth.assert_() + .about(JavaSourceSubjectFactory.javaSource()) + .that(model) + .processedWith(processors()) + .compilesWithoutError() + + // We don't generate subclasses if the model is abstract unless it has a class annotation. + val modelNotGenerated: Boolean = try { + JavaFileObjects.forResource("non-sense? Any wrong path will generate IAE") + false + } catch (e: IllegalArgumentException) { + true + } + + Assert.assertTrue(modelNotGenerated) + } + + @Test + fun testModelWithAbstractClassAndAnnotation() { + assertGeneration( + "ModelWithAbstractClassAndAnnotation.java", + "ModelWithAbstractClassAndAnnotation_.java" + ) + } + + @Test + fun testModelWithAnnotatedClassAndSuperClass() { + val model = JavaFileObjects + .forResource("ModelWithAnnotatedClassAndSuperAttributes.java".patchResource()) + + val generatedModel = JavaFileObjects + .forResource("ModelWithAnnotatedClassAndSuperAttributes_.java".patchResource()) + + val generatedSubClassModel = JavaFileObjects.forResource( + ("ModelWithAnnotatedClassAndSuperAttributes\$SubModel" + + "WithAnnotatedClassAndSuperAttributes_.java").patchResource() + ) + Truth.assert_() + .about(JavaSourceSubjectFactory.javaSource()) + .that(model) + .processedWith(processors()) + .compilesWithoutError() + .and() + .generatesSources(generatedModel, generatedSubClassModel) + } + + @Test + fun testModelWithoutSetter() { + assertGeneration("ModelWithoutSetter.java", "ModelWithoutSetter_.java") + } + + @Test + fun testModelReturningClassType() { + assertGeneration("ModelReturningClassType.java", "ModelReturningClassType_.java") + } + + @Test + fun testModelReturningClassTypeWithVarargs() { + assertGeneration( + "ModelReturningClassTypeWithVarargs.java", + "ModelReturningClassTypeWithVarargs_.java" + ) + } + + @Test + fun testModelWithVarargsConstructors() { + assertGeneration("ModelWithVarargsConstructors.java", "ModelWithVarargsConstructors_.java") + } + + @Test + fun testModelWithHolderGeneratesNewHolderMethod() { + assertGeneration("AbstractModelWithHolder.java", "AbstractModelWithHolder_.java") + } + + @Test + fun testGenerateDefaultLayoutMethod() { + assertGeneration("GenerateDefaultLayoutMethod.java", "GenerateDefaultLayoutMethod_.java") + } + + @Test + fun testGenerateDefaultLayoutMethodFailsIfLayoutNotSpecified() { + assertGenerationError( + "GenerateDefaultLayoutMethodNoLayout.java", + "Model must specify a valid layout resource" + ) + } + + @Test + fun testGeneratedDefaultMethodWithLayoutSpecifiedInParent() { + val model = JavaFileObjects + .forResource("GenerateDefaultLayoutMethodParentLayout.java".patchResource()) + + val generatedNoLayoutModel = JavaFileObjects + .forResource("GenerateDefaultLayoutMethodParentLayout\$NoLayout_.java".patchResource()) + + val generatedWithLayoutModel = + JavaFileObjects.forResource("GenerateDefaultLayoutMethodParentLayout\$WithLayout_.java".patchResource()) + + Truth.assert_() + .about(JavaSourceSubjectFactory.javaSource()) + .that(model) + .processedWith(processors()) + .compilesWithoutError() + .and() + .generatesSources(generatedNoLayoutModel, generatedWithLayoutModel) + } + + @Test + fun testGeneratedDefaultMethodWithLayoutSpecifiedInNextParent() { + val model = JavaFileObjects + .forResource("GenerateDefaultLayoutMethodNextParentLayout.java".patchResource()) + + val generatedNoLayoutModel = JavaFileObjects + .forResource("GenerateDefaultLayoutMethodNextParentLayout\$NoLayout_.java".patchResource()) + + val generatedStillNoLayoutModel = JavaFileObjects + .forResource("GenerateDefaultLayoutMethodNextParentLayout\$StillNoLayout_.java".patchResource()) + + val generatedWithLayoutModel = + JavaFileObjects.forResource("GenerateDefaultLayoutMethodNextParentLayout\$WithLayout_.java".patchResource()) + + Truth.assert_() + .about(JavaSourceSubjectFactory.javaSource()) + .that(model) + .processedWith(processors()) + .compilesWithoutError() + .and() + .generatesSources( + generatedNoLayoutModel, generatedStillNoLayoutModel, + generatedWithLayoutModel + ) + } + + @Test + fun testGeneratedDefaultMethodWithLayoutFailsIfNotSpecifiedInParent() { + assertGenerationError( + "GenerateDefaultLayoutMethodParentStillNoLayout.java", + "Model must specify a valid layout resource" + ) + } + + @Test + fun modelWithViewClickListener() { + assertGeneration("ModelWithViewClickListener.java", "ModelWithViewClickListener_.java") + } + + @Test + fun modelWithViewClickLongListener() { + val model = JavaFileObjects + .forResource("ModelWithViewLongClickListener.java".patchResource()) + + val generatedNoLayoutModel = JavaFileObjects + .forResource("ModelWithViewLongClickListener_.java".patchResource()) + + Truth.assert_() + .about(JavaSourceSubjectFactory.javaSource()) + .that(model) + .processedWith(processors()) + .compilesWithoutError() + .and() + .generatesSources(generatedNoLayoutModel) + } + + @Test + fun modelWithCheckedChangeListener() { + assertGeneration( + "ModelWithCheckedChangeListener.java", + "ModelWithCheckedChangeListener_.java" + ) + } + + @Test + fun testModelWithPrivateAttributeWithoutGetterAndSetterFails() { + assertGenerationError( + "ModelWithPrivateFieldWithoutGetterAndSetter.java", + "private fields without proper getter and setter" + ) + } + + @Test + fun testModelWithPrivateAttributeWithoutSetterFails() { + assertGenerationError( + "ModelWithPrivateFieldWithoutSetter.java", + "private fields without proper getter and setter" + ) + } + + @Test + fun testModelWithPrivateAttributeWithoutGetterFails() { + assertGenerationError( + "ModelWithPrivateFieldWithoutGetter.java", + "private fields without proper getter and setter" + ) + } + + @Test + fun testModelWithPrivateAttributeWithIsPrefixGetter() { + checkFileCompiles("ModelWithPrivateFieldWithIsPrefixGetter.java") + } + + @Test + fun testModelWithPrivateAttributeWithPrivateGetterFails() { + assertGenerationError( + "ModelWithPrivateFieldWithPrivateGetter.java", + "private fields without proper getter and setter" + ) + } + + @Test + fun testModelWithPrivateAttributeWithStaticGetterFails() { + assertGenerationError( + "ModelWithPrivateFieldWithStaticGetter.java", + "private fields without proper getter and setter" + ) + } + + @Test + fun testModelWithPrivateAttributeWithGetterWithParamsFails() { + assertGenerationError( + "ModelWithPrivateFieldWithGetterWithParams.java", + "private fields without proper getter and setter" + ) + } + + @Test + fun testModelWithPrivateAttributeWithPrivateSetterFails() { + assertGenerationError( + "ModelWithPrivateFieldWithPrivateSetter.java", + "private fields without proper getter and setter" + ) + } + + @Test + fun testModelWithPrivateAttributeWithStaticSetterFails() { + assertGenerationError( + "ModelWithPrivateFieldWithStaticSetter.java", + "private fields without proper getter and setter" + ) + } + + @Test + fun testModelWithPrivateAttributeWithGetterWithoutParamsFails() { + assertGenerationError( + "ModelWithPrivateFieldWithSettterWithoutParams.java", + "private fields without proper getter and setter" + ) + } + + @Test + fun testModelWithAllPrivateFieldTypes() { + assertGeneration( + "ModelWithAllPrivateFieldTypes.java", + "ModelWithAllPrivateFieldTypes_.java" + ) + } + + @Test + fun modelWithViewPrivateClickListener() { + assertGeneration( + "ModelWithPrivateViewClickListener.java", + "ModelWithPrivateViewClickListener_.java" + ) + } + + @Test + fun modelWithPrivateFieldWithSameAsFieldGetterAndSetterName() { + assertGeneration( + "ModelWithPrivateFieldWithSameAsFieldGetterAndSetterName.java", + "ModelWithPrivateFieldWithSameAsFieldGetterAndSetterName_.java" + ) + } + + @Test + fun testDoNotUseInToStringModel() { + assertGeneration("ModelDoNotUseInToString.java", "ModelDoNotUseInToString_.java") + } + + @Test + fun modelWithAnnotation() { + assertGeneration("ModelWithAnnotation.java", "ModelWithAnnotation_.java") + } + + @Test + fun testModelBuilderInterface() { + assertGeneration("ModelWithAllFieldTypes.java", "ModelWithAllFieldTypesBuilder.java") + } + + @Test + fun generatedEpoxyModelGroup() { + assertGeneration( + "EpoxyModelGroupWithAnnotations.java", + "EpoxyModelGroupWithAnnotations_.java" + ) + } + + @Test + fun generatedEpoxyModelWithView() { + assertGeneration( + "AbstractEpoxyModelWithView.java", + "AbstractEpoxyModelWithViewModel_.java" + ) + } +} diff --git a/epoxy-processortest/src/test/resources/AbstractEpoxyModelWithView.java b/epoxy-processortest/src/test/resources/AbstractEpoxyModelWithView.java new file mode 100644 index 0000000000..b4ebb02859 --- /dev/null +++ b/epoxy-processortest/src/test/resources/AbstractEpoxyModelWithView.java @@ -0,0 +1,17 @@ +package com.airbnb.epoxy; + +import android.view.View; +import android.view.ViewGroup; + +import com.airbnb.epoxy.EpoxyModelClass; +import com.airbnb.epoxy.EpoxyModelWithView; + +import androidx.annotation.NonNull; + +@EpoxyModelClass +public abstract class AbstractEpoxyModelWithView extends EpoxyModelWithView { + @Override + protected View buildView(@NonNull ViewGroup parent) { + return new View(parent.getContext()); + } +} diff --git a/epoxy-processortest/src/test/resources/AbstractEpoxyModelWithViewModel_.java b/epoxy-processortest/src/test/resources/AbstractEpoxyModelWithViewModel_.java new file mode 100644 index 0000000000..ccfa8c83e4 --- /dev/null +++ b/epoxy-processortest/src/test/resources/AbstractEpoxyModelWithViewModel_.java @@ -0,0 +1,245 @@ +package com.airbnb.epoxy; + +import android.view.View; +import androidx.annotation.LayoutRes; +import androidx.annotation.Nullable; +import java.lang.CharSequence; +import java.lang.Number; +import java.lang.Object; +import java.lang.Override; +import java.lang.String; + +/** + * Generated file. Do not modify! */ +public class AbstractEpoxyModelWithView_ extends AbstractEpoxyModelWithView implements GeneratedModel, AbstractEpoxyModelWithViewBuilder { + private OnModelBoundListener onModelBoundListener_epoxyGeneratedModel; + + private OnModelUnboundListener onModelUnboundListener_epoxyGeneratedModel; + + private OnModelVisibilityStateChangedListener onModelVisibilityStateChangedListener_epoxyGeneratedModel; + + private OnModelVisibilityChangedListener onModelVisibilityChangedListener_epoxyGeneratedModel; + + public AbstractEpoxyModelWithView_() { + super(); + } + + @Override + public void addTo(EpoxyController controller) { + super.addTo(controller); + addWithDebugValidation(controller); + } + + @Override + public void handlePreBind(final EpoxyViewHolder holder, final View object, final int position) { + validateStateHasNotChangedSinceAdded("The model was changed between being added to the controller and being bound.", position); + } + + @Override + public void handlePostBind(final View object, int position) { + if (onModelBoundListener_epoxyGeneratedModel != null) { + onModelBoundListener_epoxyGeneratedModel.onModelBound(this, object, position); + } + validateStateHasNotChangedSinceAdded("The model was changed during the bind call.", position); + } + + /** + * Register a listener that will be called when this model is bound to a view. + *

+ * The listener will contribute to this model's hashCode state per the {@link + * com.airbnb.epoxy.EpoxyAttribute.Option#DoNotHash} rules. + *

+ * You may clear the listener by setting a null value, or by calling {@link #reset()} */ + public AbstractEpoxyModelWithView_ onBind( + OnModelBoundListener listener) { + onMutation(); + this.onModelBoundListener_epoxyGeneratedModel = listener; + return this; + } + + @Override + public void unbind(View object) { + super.unbind(object); + if (onModelUnboundListener_epoxyGeneratedModel != null) { + onModelUnboundListener_epoxyGeneratedModel.onModelUnbound(this, object); + } + } + + /** + * Register a listener that will be called when this model is unbound from a view. + *

+ * The listener will contribute to this model's hashCode state per the {@link + * com.airbnb.epoxy.EpoxyAttribute.Option#DoNotHash} rules. + *

+ * You may clear the listener by setting a null value, or by calling {@link #reset()} */ + public AbstractEpoxyModelWithView_ onUnbind( + OnModelUnboundListener listener) { + onMutation(); + this.onModelUnboundListener_epoxyGeneratedModel = listener; + return this; + } + + @Override + public void onVisibilityStateChanged(int visibilityState, final View object) { + if (onModelVisibilityStateChangedListener_epoxyGeneratedModel != null) { + onModelVisibilityStateChangedListener_epoxyGeneratedModel.onVisibilityStateChanged(this, object, visibilityState); + } + super.onVisibilityStateChanged(visibilityState, object); + } + + /** + * Register a listener that will be called when this model visibility state has changed. + *

+ * The listener will contribute to this model's hashCode state per the {@link + * com.airbnb.epoxy.EpoxyAttribute.Option#DoNotHash} rules. + */ + public AbstractEpoxyModelWithView_ onVisibilityStateChanged( + OnModelVisibilityStateChangedListener listener) { + onMutation(); + this.onModelVisibilityStateChangedListener_epoxyGeneratedModel = listener; + return this; + } + + @Override + public void onVisibilityChanged(float percentVisibleHeight, float percentVisibleWidth, + int visibleHeight, int visibleWidth, final View object) { + if (onModelVisibilityChangedListener_epoxyGeneratedModel != null) { + onModelVisibilityChangedListener_epoxyGeneratedModel.onVisibilityChanged(this, object, percentVisibleHeight, percentVisibleWidth, visibleHeight, visibleWidth); + } + super.onVisibilityChanged(percentVisibleHeight, percentVisibleWidth, visibleHeight, visibleWidth, object); + } + + /** + * Register a listener that will be called when this model visibility has changed. + *

+ * The listener will contribute to this model's hashCode state per the {@link + * com.airbnb.epoxy.EpoxyAttribute.Option#DoNotHash} rules. + */ + public AbstractEpoxyModelWithView_ onVisibilityChanged( + OnModelVisibilityChangedListener listener) { + onMutation(); + this.onModelVisibilityChangedListener_epoxyGeneratedModel = listener; + return this; + } + + @Override + public AbstractEpoxyModelWithView_ layout(@LayoutRes int arg0) { + super.layout(arg0); + return this; + } + + @Override + public AbstractEpoxyModelWithView_ id(long id) { + super.id(id); + return this; + } + + @Override + public AbstractEpoxyModelWithView_ id(@Nullable Number... arg0) { + super.id(arg0); + return this; + } + + @Override + public AbstractEpoxyModelWithView_ id(long id1, long id2) { + super.id(id1, id2); + return this; + } + + @Override + public AbstractEpoxyModelWithView_ id(@Nullable CharSequence arg0) { + super.id(arg0); + return this; + } + + @Override + public AbstractEpoxyModelWithView_ id(@Nullable CharSequence arg0, + @Nullable CharSequence... arg1) { + super.id(arg0, arg1); + return this; + } + + @Override + public AbstractEpoxyModelWithView_ id(@Nullable CharSequence arg0, long arg1) { + super.id(arg0, arg1); + return this; + } + + @Override + public AbstractEpoxyModelWithView_ spanSizeOverride( + @Nullable EpoxyModel.SpanSizeOverrideCallback arg0) { + super.spanSizeOverride(arg0); + return this; + } + + @Override + public AbstractEpoxyModelWithView_ show() { + super.show(); + return this; + } + + @Override + public AbstractEpoxyModelWithView_ show(boolean show) { + super.show(show); + return this; + } + + @Override + public AbstractEpoxyModelWithView_ hide() { + super.hide(); + return this; + } + + @Override + public AbstractEpoxyModelWithView_ reset() { + onModelBoundListener_epoxyGeneratedModel = null; + onModelUnboundListener_epoxyGeneratedModel = null; + onModelVisibilityStateChangedListener_epoxyGeneratedModel = null; + onModelVisibilityChangedListener_epoxyGeneratedModel = null; + super.reset(); + return this; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof AbstractEpoxyModelWithView_)) { + return false; + } + if (!super.equals(o)) { + return false; + } + AbstractEpoxyModelWithView_ that = (AbstractEpoxyModelWithView_) o; + if (((onModelBoundListener_epoxyGeneratedModel == null) != (that.onModelBoundListener_epoxyGeneratedModel == null))) { + return false; + } + if (((onModelUnboundListener_epoxyGeneratedModel == null) != (that.onModelUnboundListener_epoxyGeneratedModel == null))) { + return false; + } + if (((onModelVisibilityStateChangedListener_epoxyGeneratedModel == null) != (that.onModelVisibilityStateChangedListener_epoxyGeneratedModel == null))) { + return false; + } + if (((onModelVisibilityChangedListener_epoxyGeneratedModel == null) != (that.onModelVisibilityChangedListener_epoxyGeneratedModel == null))) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int _result = super.hashCode(); + _result = 31 * _result + (onModelBoundListener_epoxyGeneratedModel != null ? 1 : 0); + _result = 31 * _result + (onModelUnboundListener_epoxyGeneratedModel != null ? 1 : 0); + _result = 31 * _result + (onModelVisibilityStateChangedListener_epoxyGeneratedModel != null ? 1 : 0); + _result = 31 * _result + (onModelVisibilityChangedListener_epoxyGeneratedModel != null ? 1 : 0); + return _result; + } + + @Override + public String toString() { + return "AbstractEpoxyModelWithView_{" + + "}" + super.toString(); + } +} \ No newline at end of file diff --git a/epoxy-sample/build.gradle b/epoxy-sample/build.gradle index bd5b76e462..265d1cdfb5 100644 --- a/epoxy-sample/build.gradle +++ b/epoxy-sample/build.gradle @@ -36,7 +36,7 @@ project.android.buildTypes.all { buildType -> requireHashCodeInEpoxyModels: "true", requireAbstractEpoxyModels : "true", implicitlyAddAutoModels : "true", - logEpoxyTimings : "true", + logEpoxyTimings : "false", enableParallelEpoxyProcessing : "true", ] } diff --git a/gradle.properties b/gradle.properties index 971a49384f..365f9ef150 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=4.0.0-beta3 +VERSION_NAME=4.0.0-beta4 GROUP=com.airbnb.android POM_DESCRIPTION=Epoxy is a system for composing complex screens with a ReyclerView in Android. POM_URL=https://github.com/airbnb/epoxy diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0ebb3108e2..21e622da69 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists