diff options
author | George Mount <mount@google.com> | 2016-09-28 19:21:41 +0000 |
---|---|---|
committer | George Mount <mount@google.com> | 2016-09-28 12:48:34 -0700 |
commit | e468b817509c5c7dd657a0cbc997452e1106aed8 (patch) | |
tree | aa194fa6a5d607d7eda7d4e11dfc2578a7eec05b | |
parent | 83bcfb874866a2f6f74f7700fe2c67f0af2a3c20 (diff) | |
download | data-binding-e468b817509c5c7dd657a0cbc997452e1106aed8.tar.gz |
Re-adds "Add support for dependent bindable properties."
This reverts commit 729298e98a96730a9fbcf8c1d2575a8f0da7cc70.
This also fixes the build break caused by the added abstract
methods in ModelMethod and ModelField.
Change-Id: I4d7d5085a1a2e02ede0662591d8ccd79770c5fb3
26 files changed, 517 insertions, 13 deletions
diff --git a/baseLibrary/src/main/java/android/databinding/Bindable.java b/baseLibrary/src/main/java/android/databinding/Bindable.java index 45436d6c..2efd2824 100644 --- a/baseLibrary/src/main/java/android/databinding/Bindable.java +++ b/baseLibrary/src/main/java/android/databinding/Bindable.java @@ -26,10 +26,31 @@ import java.lang.annotation.Target; * The Bindable annotation should be applied to any getter accessor method of an * {@link Observable} class. Bindable will generate a field in the BR class to identify * the field that has changed. + * <p> + * When applied to an accessor method, the Bindable annotation takes an optional + * list of property names that it depends on. If there is a change notification of any of the + * listed properties, this value will also be considered dirty and be refreshed. For example: + * <p><pre> + * <code>@Bindable + * public void getFirstName() { return this.firstName; } + * + * @Bindable + * public void getLastName() { return this.lastName; } + * + * @Bindable({"firstName", "lastName"}} + * public void getName() { return this.firstName + ' ' + this.lastName; } + * </code></pre> + * <p> + * Whenever either {@code firstName} or {@code lastName} has a change notification, {@code name} + * will also be considered dirty. This does not mean that + * {@link OnPropertyChangedCallback#onPropertyChanged(Observable, int)} will be notified for + * {@code BR.name}, only that binding expressions containing {@code name} will be dirtied and + * refreshed. * * @see OnPropertyChangedCallback#onPropertyChanged(Observable, int) */ @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) // this is necessary for java analyzer to work public @interface Bindable { + String[] value() default {}; } diff --git a/compilationTests/src/test/java/android/databinding/compilationTest/SimpleCompilationTest.java b/compilationTests/src/test/java/android/databinding/compilationTest/SimpleCompilationTest.java index 4b8ec9f1..516452ad 100644 --- a/compilationTests/src/test/java/android/databinding/compilationTest/SimpleCompilationTest.java +++ b/compilationTests/src/test/java/android/databinding/compilationTest/SimpleCompilationTest.java @@ -392,4 +392,84 @@ public class SimpleCompilationTest extends BaseCompilationTest { assertNotNull("Method " + method + " not found", modelMethod); } } + + @Test + public void testDependantDoesNotExist() throws Throwable { + prepareProject(); + copyResourceTo("/layout/layout_with_dependency.xml", + "/app/src/main/res/layout/layout_with_dependency.xml"); + copyResourceTo( + "/android/databinding/compilationTest/badJava/ObservableNoDependent.java", + "/app/src/main/java/android/databinding/compilationTest/badJava/MyObservable.java"); + + CompilationResult result = runGradle("assembleDebug"); + assertNotEquals(0, result.resultCode); + List<ScopedException> errors = ScopedException.extractErrors(result.error); + assertEquals(result.error, 1, errors.size()); + final ScopedException ex = errors.get(0); + final ScopedErrorReport report = ex.getScopedErrorReport(); + final File errorFile = new File(report.getFilePath()); + assertTrue(errorFile.exists()); + assertEquals(new File(testFolder, + "/app/src/main/res/layout/layout_with_dependency.xml") + .getCanonicalFile(), + errorFile.getCanonicalFile()); + assertEquals("Could not find dependent property 'notExist' referenced in " + + "@Bindable annotation on " + + "android.databinding.compilationTest.badJava.MyObservable.getField", + ex.getBareMessage()); + } + + @Test + public void testDependantNotBindable() throws Throwable { + prepareProject(); + copyResourceTo("/layout/layout_with_dependency.xml", + "/app/src/main/res/layout/layout_with_dependency.xml"); + copyResourceTo( + "/android/databinding/compilationTest/badJava/ObservableNotBindableDependent.java", + "/app/src/main/java/android/databinding/compilationTest/badJava/MyObservable.java"); + + CompilationResult result = runGradle("assembleDebug"); + assertNotEquals(0, result.resultCode); + List<ScopedException> errors = ScopedException.extractErrors(result.error); + assertEquals(result.error, 1, errors.size()); + final ScopedException ex = errors.get(0); + final ScopedErrorReport report = ex.getScopedErrorReport(); + final File errorFile = new File(report.getFilePath()); + assertTrue(errorFile.exists()); + assertEquals(new File(testFolder, + "/app/src/main/res/layout/layout_with_dependency.xml") + .getCanonicalFile(), + errorFile.getCanonicalFile()); + assertEquals("The dependent property 'otherField' referenced in " + + "@Bindable annotation on " + + "android.databinding.compilationTest.badJava.MyObservable.getField " + + "must be annotated with @Bindable", ex.getBareMessage()); + } + + @Test + public void testDependantField() throws Throwable { + prepareProject(); + copyResourceTo("/layout/layout_with_dependency.xml", + "/app/src/main/res/layout/layout_with_dependency.xml"); + copyResourceTo( + "/android/databinding/compilationTest/badJava/ObservableFieldDependent.java", + "/app/src/main/java/android/databinding/compilationTest/badJava/MyObservable.java"); + + CompilationResult result = runGradle("assembleDebug"); + assertNotEquals(0, result.resultCode); + List<ScopedException> errors = ScopedException.extractErrors(result.error); + assertEquals(result.error, 1, errors.size()); + final ScopedException ex = errors.get(0); + final ScopedErrorReport report = ex.getScopedErrorReport(); + final File errorFile = new File(report.getFilePath()); + assertTrue(errorFile.exists()); + assertEquals(new File(testFolder, + "/app/src/main/res/layout/layout_with_dependency.xml") + .getCanonicalFile(), + errorFile.getCanonicalFile()); + assertEquals("Bindable annotation with property names is only supported on methods. " + + "Field 'android.databinding.compilationTest.badJava.MyObservable.field' has " + + "@Bindable(\"otherField\")", ex.getBareMessage()); + } } diff --git a/compilationTests/src/test/resources/android/databinding/compilationTest/badJava/ObservableFieldDependent.java b/compilationTests/src/test/resources/android/databinding/compilationTest/badJava/ObservableFieldDependent.java new file mode 100644 index 00000000..78c19659 --- /dev/null +++ b/compilationTests/src/test/resources/android/databinding/compilationTest/badJava/ObservableFieldDependent.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.databinding.compilationTest.badJava; + +import android.databinding.BaseObservable; +import android.databinding.Bindable; + +public class MyObservable extends BaseObservable { + @Bindable + public String otherField; + + @Bindable("otherField") + public String field; +} diff --git a/compilationTests/src/test/resources/android/databinding/compilationTest/badJava/ObservableNoDependent.java b/compilationTests/src/test/resources/android/databinding/compilationTest/badJava/ObservableNoDependent.java new file mode 100644 index 00000000..5138b92c --- /dev/null +++ b/compilationTests/src/test/resources/android/databinding/compilationTest/badJava/ObservableNoDependent.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.databinding.compilationTest.badJava; + +import android.databinding.BaseObservable; +import android.databinding.Bindable; + +public class MyObservable extends BaseObservable { + @Bindable("notExist") + public String getField() { + return "Hello"; + } +} diff --git a/compilationTests/src/test/resources/android/databinding/compilationTest/badJava/ObservableNotBindableDependent.java b/compilationTests/src/test/resources/android/databinding/compilationTest/badJava/ObservableNotBindableDependent.java new file mode 100644 index 00000000..7992fe3e --- /dev/null +++ b/compilationTests/src/test/resources/android/databinding/compilationTest/badJava/ObservableNotBindableDependent.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.databinding.compilationTest.badJava; + +import android.databinding.BaseObservable; +import android.databinding.Bindable; + +public class MyObservable extends BaseObservable { + @Bindable("otherField") + public String getField() { + return "Hello"; + } + + public String otherField() { + return "World"; + } +} diff --git a/compilationTests/src/test/resources/layout/layout_with_dependency.xml b/compilationTests/src/test/resources/layout/layout_with_dependency.xml new file mode 100644 index 00000000..4c13e40b --- /dev/null +++ b/compilationTests/src/test/resources/layout/layout_with_dependency.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2016 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<layout xmlns:android="http://schemas.android.com/apk/res/android"> + <data> + <variable name="obj" type="android.databinding.compilationTest.badJava.MyObservable"/> + </data> + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@{obj.field}"/> +</layout>
\ No newline at end of file diff --git a/compiler/src/main/java/android/databinding/tool/Binding.java b/compiler/src/main/java/android/databinding/tool/Binding.java index 2395f23a..5f193a3e 100644 --- a/compiler/src/main/java/android/databinding/tool/Binding.java +++ b/compiler/src/main/java/android/databinding/tool/Binding.java @@ -156,7 +156,7 @@ public class Binding implements LocationScopeProvider { // Now try with the value object directly mSetterCall = SetterStore.get(modelAnalyzer).getSetterCall(mName, viewType, mExpr.getResolvedType(), mExpr.getModel().getImports()); - if (warn) { + if (warn && mSetterCall != null) { L.w(ErrorMessages.OBSERVABLE_FIELD_USE, mSetterCall.getDescription()); } } diff --git a/compiler/src/main/java/android/databinding/tool/expr/FieldAccessExpr.java b/compiler/src/main/java/android/databinding/tool/expr/FieldAccessExpr.java index be48cd4a..d4a54de0 100644 --- a/compiler/src/main/java/android/databinding/tool/expr/FieldAccessExpr.java +++ b/compiler/src/main/java/android/databinding/tool/expr/FieldAccessExpr.java @@ -16,6 +16,7 @@ package android.databinding.tool.expr; +import android.databinding.Bindable; import android.databinding.tool.Binding; import android.databinding.tool.BindingTarget; import android.databinding.tool.InverseBinding; @@ -26,16 +27,20 @@ import android.databinding.tool.reflection.Callable; import android.databinding.tool.reflection.Callable.Type; import android.databinding.tool.reflection.ModelAnalyzer; import android.databinding.tool.reflection.ModelClass; +import android.databinding.tool.reflection.ModelField; import android.databinding.tool.store.SetterStore; import android.databinding.tool.store.SetterStore.BindingGetterCall; import android.databinding.tool.util.BrNameUtil; import android.databinding.tool.util.L; import android.databinding.tool.util.Preconditions; +import android.databinding.tool.util.StringUtils; import android.databinding.tool.writer.KCode; import com.google.common.collect.Lists; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; public class FieldAccessExpr extends MethodBaseExpr { // notification name for the field. Important when we map this to a method w/ different name @@ -157,6 +162,70 @@ public class FieldAccessExpr extends MethodBaseExpr { } } + /** + * @return The list of all properties that dirty this expression. This will also contain the + * the BR identifier for this property if there is one. If the property is not bindable, + * it will contain an empty list. + */ + public String[] getDirtyingProperties() { + String[] names = null; + if (mGetter != null && mGetter.canBeInvalidated()) { + if (mGetter.type == Type.FIELD) { + if (mGetter.bindableAnnotation != null && + mGetter.bindableAnnotation.value().length != 0) { + L.e("Bindable annotation with property names is only supported on methods. " + + "Field '%s.%s' has @Bindable(\"%s\")", + getTarget().getResolvedType().toJavaCode(), mGetter.name, + StringUtils.join(mGetter.bindableAnnotation.value(), "\", \"")); + } + } else if (mGetter.method != null && mGetter.canBeInvalidated() && + mGetter.bindableAnnotation != null) { + String[] dependencies = mGetter.bindableAnnotation.value(); + validateDependencies(dependencies); + names = new String[dependencies.length + 1]; + for (int i = 0; i < dependencies.length; i++) { + names[i] = "BR." + dependencies[i]; + } + names[dependencies.length] = getBrName(); + } + } + if (names == null) { + String br = getBrName(); + if (br == null) { + names = new String[0]; + } else { + names = new String[] { br }; + } + } + return names; + } + + /** + * Validates the dependent properties -- they must exist and be Bindable. + */ + private void validateDependencies(String[] dependencies) { + try { + Scope.enter(this); + Arrays.stream(dependencies).forEach(field -> { + ModelClass resolvedType = getTarget().getResolvedType(); + Callable getter = resolvedType.findGetterOrField(field, mGetter.isStatic()); + if (getter == null) { + L.e("Could not find dependent property '%s' referenced in " + + "@Bindable annotation on %s.%s", field, + mGetter.method.getDeclaringClass().toJavaCode(), + mGetter.method.getName()); + } else if (!getter.canBeInvalidated()) { + L.e("The dependent property '%s' referenced in @Bindable annotation on " + + "%s.%s must be annotated with @Bindable", field, + mGetter.method.getDeclaringClass().toJavaCode(), + mGetter.method.getName()); + } + }); + } finally { + Scope.exit(); + } + } + @Override protected ModelClass resolveType(ModelAnalyzer modelAnalyzer) { if (mIsListener) { diff --git a/compiler/src/main/java/android/databinding/tool/expr/MethodCallExpr.java b/compiler/src/main/java/android/databinding/tool/expr/MethodCallExpr.java index dfeec181..dea19925 100644 --- a/compiler/src/main/java/android/databinding/tool/expr/MethodCallExpr.java +++ b/compiler/src/main/java/android/databinding/tool/expr/MethodCallExpr.java @@ -186,7 +186,7 @@ public class MethodCallExpr extends Expr { flags |= STATIC; } mGetter = new Callable(Type.METHOD, mMethod.getName(), null, mMethod.getReturnType(args), - mMethod.getParameterTypes().length, flags, mMethod); + mMethod.getParameterTypes().length, flags, mMethod, null); } return mGetter.resolvedType; } diff --git a/compiler/src/main/java/android/databinding/tool/reflection/Callable.java b/compiler/src/main/java/android/databinding/tool/reflection/Callable.java index 5088523d..54db94e3 100644 --- a/compiler/src/main/java/android/databinding/tool/reflection/Callable.java +++ b/compiler/src/main/java/android/databinding/tool/reflection/Callable.java @@ -15,6 +15,8 @@ */ package android.databinding.tool.reflection; +import android.databinding.Bindable; + import org.jetbrains.annotations.Nullable; public class Callable { @@ -42,8 +44,10 @@ public class Callable { private final int mParameterCount; + public final Bindable bindableAnnotation; + public Callable(Type type, String name, String setterName, ModelClass resolvedType, - int parameterCount, int flags, ModelMethod method) { + int parameterCount, int flags, ModelMethod method, Bindable bindable) { this.type = type; this.name = name; this.resolvedType = resolvedType; @@ -51,6 +55,7 @@ public class Callable { this.setterName = setterName; mFlags = flags; this.method = method; + this.bindableAnnotation = bindable; } public String getTypeCodeName() { diff --git a/compiler/src/main/java/android/databinding/tool/reflection/InjectedField.java b/compiler/src/main/java/android/databinding/tool/reflection/InjectedField.java index 85719f1c..82db70b1 100644 --- a/compiler/src/main/java/android/databinding/tool/reflection/InjectedField.java +++ b/compiler/src/main/java/android/databinding/tool/reflection/InjectedField.java @@ -16,6 +16,8 @@ package android.databinding.tool.reflection; +import android.databinding.Bindable; + import java.util.Map; /** diff --git a/compiler/src/main/java/android/databinding/tool/reflection/InjectedMethod.java b/compiler/src/main/java/android/databinding/tool/reflection/InjectedMethod.java index f47442b7..a91ae109 100644 --- a/compiler/src/main/java/android/databinding/tool/reflection/InjectedMethod.java +++ b/compiler/src/main/java/android/databinding/tool/reflection/InjectedMethod.java @@ -16,6 +16,8 @@ package android.databinding.tool.reflection; +import android.databinding.Bindable; + import java.util.List; import java.util.Map; diff --git a/compiler/src/main/java/android/databinding/tool/reflection/ModelClass.java b/compiler/src/main/java/android/databinding/tool/reflection/ModelClass.java index 813b72e1..d408405e 100644 --- a/compiler/src/main/java/android/databinding/tool/reflection/ModelClass.java +++ b/compiler/src/main/java/android/databinding/tool/reflection/ModelClass.java @@ -15,6 +15,7 @@ */ package android.databinding.tool.reflection; +import android.databinding.Bindable; import android.databinding.tool.reflection.Callable.Type; import android.databinding.tool.util.L; import android.databinding.tool.util.StringUtils; @@ -417,7 +418,7 @@ public abstract class ModelClass { public Callable findGetterOrField(String name, boolean staticOnly) { if ("length".equals(name) && isArray()) { return new Callable(Type.FIELD, name, null, - ModelAnalyzer.getInstance().loadPrimitive("int"), 0, 0, null); + ModelAnalyzer.getInstance().loadPrimitive("int"), 0, 0, null, null); } String capitalized = StringUtils.capitalize(name); String[] methodNames = { @@ -435,8 +436,10 @@ public abstract class ModelClass { if (method.isStatic()) { flags |= STATIC; } + final Bindable bindable; if (method.isBindable()) { flags |= CAN_BE_INVALIDATED; + bindable = method.getBindableAnnotation(); } else { // if method is not bindable, look for a backing field final ModelField backingField = getField(name, true, method.isStatic()); @@ -444,13 +447,16 @@ public abstract class ModelClass { backingField == null ? "NOT FOUND" : backingField.getName()); if (backingField != null && backingField.isBindable()) { flags |= CAN_BE_INVALIDATED; + bindable = backingField.getBindableAnnotation(); + } else { + bindable = null; } } final ModelMethod setterMethod = findSetter(method, name); final String setterName = setterMethod == null ? null : setterMethod.getName(); final Callable result = new Callable(Callable.Type.METHOD, methodName, setterName, method.getReturnType(null), method.getParameterTypes().length, - flags, method); + flags, method, bindable); return result; } } @@ -484,7 +490,8 @@ public abstract class ModelClass { if (publicField.isBindable()) { flags |= CAN_BE_INVALIDATED; } - return new Callable(Callable.Type.FIELD, name, setterFieldName, fieldType, 0, flags, null); + return new Callable(Callable.Type.FIELD, name, setterFieldName, fieldType, 0, flags, null, + publicField.getBindableAnnotation()); } public ModelMethod findInstanceGetter(String name) { diff --git a/compiler/src/main/java/android/databinding/tool/reflection/ModelField.java b/compiler/src/main/java/android/databinding/tool/reflection/ModelField.java index 0cde85b0..2baef1e8 100644 --- a/compiler/src/main/java/android/databinding/tool/reflection/ModelField.java +++ b/compiler/src/main/java/android/databinding/tool/reflection/ModelField.java @@ -15,6 +15,8 @@ */ package android.databinding.tool.reflection; +import android.databinding.Bindable; + public abstract class ModelField { /** @@ -46,4 +48,11 @@ public abstract class ModelField { * @return The declared type of the field variable. */ public abstract ModelClass getFieldType(); + + /** + * @return the Bindable annotation on the field or null if there isn't one. + */ + public Bindable getBindableAnnotation() { + return null; + } } diff --git a/compiler/src/main/java/android/databinding/tool/reflection/ModelMethod.java b/compiler/src/main/java/android/databinding/tool/reflection/ModelMethod.java index 01500706..5605245a 100644 --- a/compiler/src/main/java/android/databinding/tool/reflection/ModelMethod.java +++ b/compiler/src/main/java/android/databinding/tool/reflection/ModelMethod.java @@ -44,6 +44,13 @@ public abstract class ModelMethod { public abstract boolean isBindable(); /** + * @return the Bindable annotation on the method or null if it doesn't exist. + */ + public Bindable getBindableAnnotation() { + return null; + } + + /** * Since when this method is available. Important for Binding expressions so that we don't * call non-existing APIs when setting UI. * diff --git a/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationField.java b/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationField.java index 9373c6c9..be860570 100644 --- a/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationField.java +++ b/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationField.java @@ -70,6 +70,11 @@ class AnnotationField extends ModelField { } @Override + public Bindable getBindableAnnotation() { + return mField.getAnnotation(Bindable.class); + } + + @Override public boolean equals(Object obj) { if (obj instanceof AnnotationField) { AnnotationField that = (AnnotationField) obj; diff --git a/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationMethod.java b/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationMethod.java index 66c1dbc5..839aa282 100644 --- a/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationMethod.java +++ b/compiler/src/main/java/android/databinding/tool/reflection/annotation/AnnotationMethod.java @@ -147,6 +147,11 @@ class AnnotationMethod extends ModelMethod { } @Override + public Bindable getBindableAnnotation() { + return mExecutableElement.getAnnotation(Bindable.class); + } + + @Override public int getMinApi() { if (mApiLevel == -1) { mApiLevel = SdkUtil.getMinApi(this); diff --git a/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt b/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt index 3820084d..2da9367d 100644 --- a/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt +++ b/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt @@ -749,14 +749,15 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) { block("switch (fieldId)", { val accessedFields: List<FieldAccessExpr> = it.parents.filterIsInstance(FieldAccessExpr::class.java) accessedFields.filter { it.isUsed && it.hasBindableAnnotations() } - .groupBy { it.brName } + .flatMap { expr -> expr.dirtyingProperties.map { Pair(it, expr)} } + .groupBy { it.first } .forEach { // If two expressions look different but resolve to the same method, // we are not yet able to merge them. This is why we merge their // flags below. block("case ${it.key}:") { block("synchronized(this)") { - val flagSet = it.value.foldRight(FlagSet()) { l, r -> l.invalidateFlagSet.or(r) } + val flagSet = it.value.foldRight(FlagSet()) { l, r -> l.second.invalidateFlagSet.or(r) } mDirtyFlags.mapOr(flagSet) { suffix, index -> tab("${mDirtyFlags.localValue(index)} |= ${flagSet.localValue(index)};") diff --git a/compiler/src/test/java/android/databinding/tool/reflection/java/JavaField.java b/compiler/src/test/java/android/databinding/tool/reflection/java/JavaField.java index 6821f168..41f68488 100644 --- a/compiler/src/test/java/android/databinding/tool/reflection/java/JavaField.java +++ b/compiler/src/test/java/android/databinding/tool/reflection/java/JavaField.java @@ -56,4 +56,9 @@ public class JavaField extends ModelField { public ModelClass getFieldType() { return new JavaClass(mField.getType()); } + + @Override + public Bindable getBindableAnnotation() { + return mField.getAnnotation(Bindable.class); + } } diff --git a/compiler/src/test/java/android/databinding/tool/reflection/java/JavaMethod.java b/compiler/src/test/java/android/databinding/tool/reflection/java/JavaMethod.java index b7b626c1..46f7bf3a 100644 --- a/compiler/src/test/java/android/databinding/tool/reflection/java/JavaMethod.java +++ b/compiler/src/test/java/android/databinding/tool/reflection/java/JavaMethod.java @@ -88,6 +88,11 @@ public class JavaMethod extends ModelMethod { } @Override + public Bindable getBindableAnnotation() { + return mMethod.getAnnotation(Bindable.class); + } + + @Override public int getMinApi() { return SdkUtil.getMinApi(this); } diff --git a/compilerCommon/src/main/java/android/databinding/tool/util/StringUtils.java b/compilerCommon/src/main/java/android/databinding/tool/util/StringUtils.java index 5774183a..12b61d32 100644 --- a/compilerCommon/src/main/java/android/databinding/tool/util/StringUtils.java +++ b/compilerCommon/src/main/java/android/databinding/tool/util/StringUtils.java @@ -19,6 +19,9 @@ package android.databinding.tool.util; import com.google.common.base.StandardSystemProperty; import com.google.common.base.Strings; +import java.util.Arrays; +import java.util.stream.Collectors; + public class StringUtils { public static final String LINE_SEPARATOR = StandardSystemProperty.LINE_SEPARATOR.value(); @@ -74,7 +77,19 @@ public class StringUtils { .replace(LFEED_ENTITY, "\n"); } - private StringUtils() { + /** + * Merges the array of strings together using a separator string. + * @param strings The array of strings to merge into one string. + * @param sep The separator between the string values. + * @return The merge of all string values, separated by sep or null if strings is null + */ + public static <T extends CharSequence> String join(T[] strings, String sep) { + if (strings == null) { + return null; + } + return Arrays.stream(strings).collect(Collectors.joining(sep)); } + private StringUtils() { + } } diff --git a/integration-tests/MultiModuleTestApp/testlibrary/build.gradle b/integration-tests/MultiModuleTestApp/testlibrary/build.gradle index 38bf3a6c..62f97fb8 100644 --- a/integration-tests/MultiModuleTestApp/testlibrary/build.gradle +++ b/integration-tests/MultiModuleTestApp/testlibrary/build.gradle @@ -21,7 +21,7 @@ android { buildToolsVersion dataBindingConfig.buildToolsVersion defaultConfig { - minSdkVersion 7 + minSdkVersion 9 targetSdkVersion dataBindingConfig.targetSdkVersion versionCode 1 versionName "1.0" diff --git a/integration-tests/TestApp/app/src/androidTest/AndroidManifest.xml b/integration-tests/TestApp/app/src/androidTest/AndroidManifest.xml index 2acf5dc4..b8bb7cc4 100644 --- a/integration-tests/TestApp/app/src/androidTest/AndroidManifest.xml +++ b/integration-tests/TestApp/app/src/androidTest/AndroidManifest.xml @@ -13,7 +13,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="android.databinding.testapp"> - <uses-sdk android:minSdkVersion="7" tools:overrideLibrary="android.support.test, + <uses-sdk android:minSdkVersion="9" tools:overrideLibrary="android.support.test, android.app, android.support.test.rule, android.support.test.espresso, android.support.test.espresso.idling"/> <application android:allowBackup="true" @@ -25,4 +25,4 @@ android:screenOrientation="landscape"/> </application> -</manifest>
\ No newline at end of file +</manifest> diff --git a/integration-tests/TestApp/app/src/androidTestApi9/java/android/databinding/testapp/BindingReliesOnTest.java b/integration-tests/TestApp/app/src/androidTestApi9/java/android/databinding/testapp/BindingReliesOnTest.java new file mode 100644 index 00000000..78e4cffb --- /dev/null +++ b/integration-tests/TestApp/app/src/androidTestApi9/java/android/databinding/testapp/BindingReliesOnTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.databinding.testapp; + +import android.databinding.testapp.databinding.ReliesOnBinding; +import android.databinding.testapp.BR; +import android.databinding.testapp.vo.BasicObject; +import android.test.UiThreadTest; + +public class BindingReliesOnTest extends BaseDataBinderTest<ReliesOnBinding> { + + + public BindingReliesOnTest() { + super(ReliesOnBinding.class); + } + + protected void setUp() throws Exception { + initBinder(null); + } + + @UiThreadTest + public void testReliesOn() { + BasicObject obj = new BasicObject(); + obj.setField1("Hello"); + obj.setField2("World"); + obj.field3 = "1"; + mBinder.setObj(obj); + mBinder.executePendingBindings(); + + assertEquals("Hello", mBinder.field1.getText().toString()); + assertEquals("World", mBinder.field2.getText().toString()); + assertEquals("Hello World 1", mBinder.combo.getText().toString()); + assertEquals("+1", mBinder.field4.getText().toString()); + + obj.setField1("Goodbye"); + mBinder.executePendingBindings(); + + assertEquals("Goodbye", mBinder.field1.getText().toString()); + assertEquals("World", mBinder.field2.getText().toString()); + assertEquals("Goodbye World 1", mBinder.combo.getText().toString()); + assertEquals("+1", mBinder.field4.getText().toString()); + + obj.setField2("Cruel"); + mBinder.executePendingBindings(); + + assertEquals("Goodbye", mBinder.field1.getText().toString()); + assertEquals("Cruel", mBinder.field2.getText().toString()); + assertEquals("Goodbye Cruel 1", mBinder.combo.getText().toString()); + assertEquals("+1", mBinder.field4.getText().toString()); + + obj.field3 = "World"; + obj.notifyPropertyChanged(BR.field3); + mBinder.executePendingBindings(); + + assertEquals("Goodbye", mBinder.field1.getText().toString()); + assertEquals("Cruel", mBinder.field2.getText().toString()); + assertEquals("Goodbye Cruel World", mBinder.combo.getText().toString()); + assertEquals("+World", mBinder.field4.getText().toString()); + + obj.setJoin("-"); + mBinder.executePendingBindings(); + + assertEquals("Goodbye", mBinder.field1.getText().toString()); + assertEquals("Cruel", mBinder.field2.getText().toString()); + assertEquals("Goodbye-Cruel-World", mBinder.combo.getText().toString()); + assertEquals("-World", mBinder.field4.getText().toString()); + } +} diff --git a/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/vo/BasicObject.java b/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/vo/BasicObject.java index 5f332ef6..f52faefc 100644 --- a/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/vo/BasicObject.java +++ b/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/vo/BasicObject.java @@ -25,6 +25,14 @@ public class BasicObject extends BaseObservable { @Bindable private String mField2; + @Bindable + public String field3; + + @Bindable("field3") + private String mField4 = "+"; + + private String mJoin = " "; + public String getField1() { return mField1; } @@ -40,7 +48,23 @@ public class BasicObject extends BaseObservable { public void setField2(String field2) { this.mField2 = field2; - notifyPropertyChanged(BR.field1); + notifyPropertyChanged(BR.field2); + } + + @Bindable({"field1", "field2", "field3"}) + public String getCombo() { + return mField1 + mJoin + mField2 + mJoin + field3; + } + + public void setJoin(String join) { + mJoin = join; + mField4 = join; + notifyPropertyChanged(BR.combo); + notifyPropertyChanged(BR.field4); + } + + public String getField4() { + return mField4 + field3; } @Bindable diff --git a/integration-tests/TestApp/app/src/main/res/layout/relies_on.xml b/integration-tests/TestApp/app/src/main/res/layout/relies_on.xml new file mode 100644 index 00000000..e28b1b71 --- /dev/null +++ b/integration-tests/TestApp/app/src/main/res/layout/relies_on.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2016 The Android Open Source Project + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<layout xmlns:android="http://schemas.android.com/apk/res/android"> + + <data> + <variable + name="obj" + type="android.databinding.testapp.vo.BasicObject" /> + </data> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <TextView + android:id="@+id/field1" + android:text="@{obj.field1}" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/field2" + android:text="@{obj.field2}" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/combo" + android:text="@{obj.combo}" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/field4" + android:text="@{obj.field4}" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + </LinearLayout> +</layout>
\ No newline at end of file |